Skip to content

Type Confusion via Infinity Leading to Global Scope Hijacking #71

Description

@hkbinbin

Summary

A type confusion vulnerability exists in the Elk JavaScript engine's NaN boxing implementation. The IEEE 754 representation of positive Infinity is incorrectly interpreted as an internal object type pointing to offset 0, which is the global scope object. This allows attackers to read, shadow, or hijack any global variable or function from JavaScript code.

Root Cause

Elk uses NaN boxing to encode JavaScript values into 64-bit doubles. The type is encoded in bits 48-51 when the value is a NaN (exponent bits all 1s):

// elk.c line 138-140
static jsval_t mkval(uint8_t type, uint64_t data) { 
    return ((jsval_t) 0x7ff0U << 48U) | ((jsval_t) (type) << 48) | (data & 0xffffffffffffUL); 
}
static bool is_nan(jsval_t v) { return (v >> 52U) == 0x7ffU; }
static uint8_t vtype(jsval_t v) { return is_nan(v) ? ((v >> 48U) & 15U) : (uint8_t) T_NUM; }

The problem is that IEEE 754 +Infinity has the bit pattern 0x7FF0000000000000, which:

  • Passes the is_nan() check (exponent bits are all 1s)
  • Has type bits = 0, which equals T_OBJ (object type)
  • Has data bits = 0, which points to offset 0 in the JS heap

Offset 0 is where the global scope object is allocated during js_create():

// elk.c line 1333
js->scope = mkobj(js, 0);  // Create global scope at offset 0

Proof of Concept

// Create Infinity - this is misinterpreted as T_OBJ pointing to global scope
let inf = 1e300 * 1e300;

// Verify the type confusion
print("typeof Infinity:", typeof(inf));  // Outputs: "object" (should be "number")

Expected output

"typeof Infinity:" "object"

Impact

Function Hijacking: Intercept and modify any global function calls, including security-critical C functions imported by the host
Security Bypass: Override authentication/authorization check functions
Logic Tampering: Modify application behavior by shadowing global variables
Information Disclosure: Read values of global variables

Suggested Fix

Add an explicit check for Infinity in the vtype() function:

// Option 1: Check for Infinity before NaN boxing interpretation
static uint8_t vtype(jsval_t v) { 
    // Infinity (0x7FF0000000000000) should be treated as T_NUM, not T_OBJ
    if (v == 0x7FF0000000000000ULL) return T_NUM;  // +Infinity
    return is_nan(v) ? ((v >> 48U) & 15U) : (uint8_t) T_NUM; 
}

// Option 2: Reserve type 0 and shift all type values
// This requires more extensive changes but is more robust

Alternative fix - modify the NaN boxing scheme to avoid collision:

// Use a different bit pattern that doesn't collide with IEEE 754 special values
// For example, use 0x7FF1 as the NaN marker instead of 0x7FF0
static jsval_t mkval(uint8_t type, uint64_t data) { 
    return ((jsval_t) 0x7ff1U << 48U) | ((jsval_t) (type) << 48) | (data & 0xffffffffffffUL); 
}
static bool is_nan(jsval_t v) { return (v >> 52U) == 0x7ffU && ((v >> 48U) & 0xF) != 0; }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions