CVE-2026-24874

False Idols: Type Confusion in Xray-Monolith's LuaJIT Engine (CVE-2026-24874)

Alon Barad
Alon Barad
Software Engineer

Jan 28, 2026·7 min read·2 visits

Executive Summary (TL;DR)

The `debug.getinfo` function in xray-monolith's LuaJIT implementation relied on a development-only assertion (`api_check`) to validate input types. In production builds, this check vanished, allowing attackers to pass non-function objects (like strings) to a function that expected a function pointer. This type confusion allows for memory corruption and RCE.

A critical type confusion vulnerability in the embedded LuaJIT component of xray-monolith allows attackers to treat arbitrary data as function pointers. By exploiting a disabled debug assertion, malicious actors can bypass type safety, leading to arbitrary memory read/write and potential Remote Code Execution.

The Hook: When Assertions Go Missing

In the world of C programming, there is a deadly assumption that many developers make: that assert() protects them. It doesn't. Assertions are for debugging logic during development; they are ghosts that vanish when the compiler is set to Release mode. CVE-2026-24874 is a textbook example of what happens when security-critical logic relies on a ghost.

The target here is themrdemonized/xray-monolith, a robust system that, like many monoliths, swallowed a smaller, more complex creature whole: the LuaJIT engine. LuaJIT is famous for its speed, but speed often comes with the motto 'move fast and break things.' In this case, the xray-monolith team integrated a version of LuaJIT that contained a lurking flaw in its debug API—a flaw that turns the seemingly harmless act of inspecting a function into a weapon of mass destruction for memory safety.

Why does this matter? Because xray-monolith is often deployed in environments where user-supplied scripts or configurations might interact with the Lua state. If you can trick the engine into confusing a piece of text you wrote for a piece of executable code, you own the process. It's the binary exploitation equivalent of telling a lie so convincing that reality rearranges itself to match it.

The Flaw: A Case of Mistaken Identity

The vulnerability resides in src/3rd party/luajit-2/src/lj_debug.c, specifically within the lj_debug_getinfo function. This function is the C-side implementation of Lua's debug.getinfo. It allows developers to retrieve metadata about a function: its name, where it was defined, its current line number, etc. One specific feature of this API is the > mode, which tells the function: "I have put the function I want you to inspect on top of the stack. Please pop it off and tell me about it."

Here is where the logic breaks down. The code needs to verify that the item on top of the stack is actually a function before it starts treating it like one. The original developer wrote this check using a macro called api_check.

> [!WARNING] > The Fatal Flaw: api_check(L, tvisfunc(func)) expands to ((void)0) (essentially nothing) in standard release builds.

Because the check is compiled out, the code proceeds blindly. It takes whatever TValue (Lua's tagged value structure) is at L->top - 1 and forcibly casts it to a GCfunc pointer using the funcV macro. If an attacker puts a string, a table, or a number there, the engine doesn't care. It grabs the raw bits of that value and dereferences them as if they were a function structure.

The Code: The Smoking Gun

Let's look at the C code to see exactly how this manifests. The use of macros in LuaJIT is heavy, intended to keep the JIT compiler blazing fast, but here it obscures a critical safety failure.

The Vulnerable Code

int lj_debug_getinfo(lua_State *L, const char *what, lj_Debug *ar, int ext)
{
  GCfunc *fn;
  if (*what == '>') {
    TValue *func = L->top - 1;
    // INSECURE: This check effectively deletes itself in production
    api_check(L, tvisfunc(func)); 
    
    // TYPE CONFUSION: 'func' is cast to GCfunc* blindly
    fn = funcV(func);
    L->top--;
    what++;
  }
  // ... subsequent code dereferences 'fn' ...
}

When api_check is optimized away, funcV(func) just takes the payload of the TValue and returns it. If the TValue was a Lua String, fn now points to the string's data. If the TValue was a number, fn points to whatever memory address that number represents.

The Fix

The patch in Pull Request #399 is simple but vital. It replaces the macro with a hard, runtime conditional check that survives compiler optimization.

--- a/src/3rd party/luajit-2/src/lj_debug.c
+++ b/src/3rd party/luajit-2/src/lj_debug.c
@@ -440,7 +440,8 @@ int lj_debug_getinfo(lua_State *L, const char *what, lj_Debug *ar, int ext)
   GCfunc *fn;
   if (*what == '>') {
     TValue *func = L->top - 1;
-    api_check(L, tvisfunc(func));
+    if (!tvisfunc(func)) // FIX: Explicit runtime check
+      return 0;
     fn = funcV(func);
     L->top--;
     what++;

By returning 0 (failure) when the type is incorrect, the function fails gracefully instead of crashing or corrupting memory. This is the difference between a "Lua Error" and a "Segfault" (or worse).

The Exploit: Faking Functionality

Exploiting this requires an attacker to control the Lua stack before calling debug.getinfo. This is trivial if the attacker can execute arbitrary Lua scripts (e.g., via a plugin system or modding API). The goal is to create a "fake" function object in memory that gives us a powerful primitive, like an arbitrary read.

The Attack Chain

  1. Heap Feng Shui: The attacker allocates a Lua string (or uses FFI if available) to create a block of memory that mimics the structure of a GCfunc object. This fake structure contains pointers to other fake objects (like a GCproto).
  2. The Trigger: The attacker pushes this string (or a pointer to it) onto the stack.
  3. The Confusion: The attacker calls debug.getinfo('>', 'n'). The 'n' flag asks for the function's name.
  4. The Read:
    • lj_debug_getinfo pops the string but thinks it's a GCfunc.
    • It follows the pointer in the fake GCfunc to the fake GCproto.
    • It reads the name string pointer from the fake GCproto.
    • It copies the data from that pointer back to the user.
-- CONCEPTUAL EXPLOIT (Simplified)
-- Assume we have a way to get the address of a string via FFI or unintended leak
local fake_struct = "\x00\x00\x00..." -- Crafted bytes mimicking GCfunc
local exploit_target = 0xdeadbeef -- Address we want to read
 
-- Push the fake object (which is just a string/number to Lua)
-- Trigger the bug
local success, info = pcall(debug.getinfo, '>', fake_struct)
 
-- If successful, 'info' might contain data leaked from 0xdeadbeef

This gives the attacker an Arbitrary Read primitive. By reading pointers, they can defeat ASLR. Once ASLR is defeated, they can craft a more complex fake object that, when debug.getinfo attempts to write to it (e.g., caching line numbers), triggers an Arbitrary Write or execution flow hijack.

The Impact: From Crash to RCE

Why is this rated CVSS 9.1? Because in modern exploitation, a type confusion bug in a script engine is often the "Holy Grail." It completely breaks the sandbox.

Confidentiality: High. An attacker can read any memory in the xray-monolith process. This includes API keys, database credentials, configuration secrets, and the source code of other scripts.

Integrity: High. By upgrading the read primitive to a write primitive (which is often possible by tricking the garbage collector or using other debug flags that write to the function struct), an attacker can overwrite function pointers. They could overwrite the print function's native pointer to point to system(), and then simply call print("rm -rf /").

Availability: While often overlooked, the simplest outcome of this bug is a crash. If the type confusion causes the engine to dereference an invalid pointer, the entire xray-monolith service goes down. However, sophisticated attackers will prioritize stealthy persistence over a loud crash.

The Fix: Stopping the Bleeding

If you are running xray-monolith, you need to update immediately. The vulnerability is patched in versions released after December 30, 2025.

Mitigation Strategy

  1. Patch: Update to the latest version of xray-monolith. Verify the version is >= 2025.12.30.
  2. Restrict Debug Access: If you cannot patch immediately, you must restrict access to the debug library within your Lua environments. In many Lua configurations, you can sandbox the environment to remove debug entirely:
    local sandbox_env = {
      print = print,
      pairs = pairs,
      -- Do explicitly NOT include 'debug'
    }
  3. WAF Rules: While difficult to detect at the network layer (since the payload is valid Lua code), you can look for suspicious script patterns involving debug.getinfo calls with the > argument if you have visibility into the scripts being submitted.

This vulnerability serves as a stark reminder: Never trust macros for security boundaries. Explicit checks save lives—or at least, they save your uptime.

Fix Analysis (1)

Technical Appendix

CVSS Score
9.1/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

Affected Systems

themrdemonized/xray-monolith < 2025.12.30Embedded LuaJIT implementations derived from vulnerable versions

Affected Versions Detail

Product
Affected Versions
Fixed Version
xray-monolith
themrdemonized
< 2025.12.302025.12.30
AttributeDetail
CWE IDCWE-843
Attack VectorNetwork
CVSS9.1 (Critical)
ImpactRCE / Memory Disclosure
Vulnerability TypeType Confusion
ComponentLuaJIT (lj_debug.c)
CWE-843
Access of Resource Using Incompatible Type ('Type Confusion')

The program allocates or initializes a resource such as a pointer, object, or variable using one type, but it later accesses that resource using a type that is incompatible with the original type.

Vulnerability Timeline

Patch Submitted (PR #399)
2025-12-30
CVE Published
2026-01-27

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.