Introduction

If someone wants to view a web page with his phone or computer, he will use a web browser. You also might be reading this article with a web browser. All the commodity web browsers in the world (i.e., Microsoft Edge, Apple Safari, Google Chrome, and Mozilla Firefox) implement their own JavaScript engines to support client-side scripting language for dynamic interaction with a client. That is the reason why the JavaScript engine is one of the most popular targets of both hackers and security researchers.

In this post, we introduce an interesting security vulnerability in ChakraCore (JS engine for Edge) we found during our research, CVE-2019-0609. Although the root cause of this bug is not so complicated, it was located quiet deeply unlike other interpreter bugs. Thus, this bug was hard to catch and long-lasting. We guess this is the reason why Microsoft paid the maximum amount of payment to us for the bug bounty.

Analyzing CVE-2019-0609, a use-after-unmap vulnerability

The first insight

Let's start with the assertion hit by the PoC in debug build.

ASSERTION 264714: (/home/soyeon/jsfuzz/js-static/engines/chakracore-1.11.3/lib/Runtime/Library/StackScriptFunction.cpp, line 249) stackFunction->boxedScriptFunction == boxedFunction
 Failure: (stackFunction->boxedScriptFunction == boxedFunction)
[1]    264714 illegal hardware instruction  ~/jsfuzz/js-static/engines/chakracore-1.11.3/out/Debug/ch 35977387.js

From the assertion, we can infer that the PoC has hit an issue while boxing a JavaScript function. No doubt you will be curious about what boxing is, as I did. So, what is boxing in JavaScript?

Boxing in JavaScript

If you declare an object such as a Number or a Function inside the global code or a function, they will be allocated on the stack normally. However, they should be located on the heap for some case, such as referencing same objects with different pointers or scope escaping of an object. For example, if a function allocated on the stack is returned, the function should be boxed to avoid scope escaping. This can happen in JavaScript because a function is a first-class object in JavaScript.

function test() {
    function stackFun(){}
    function heapFun(){print("hi!");}
    return heapFun;
}

test()();
// output : hi!

In the above code, function stackFun and heapFun would be located on the stack when they are declared. Further, the function heapFun should be moved to the heap to avoid pointing the address on the stack of function test, as it is returned to the outside of function test. The behavior that the JS engine moves the object to heap from stack, is called boxing. It is similar to the concept of boxing in Java.

What makes the assertion

Based on the concept of boxing, we can infer that a scriptFunction needs boxing but it failed for some reason. For details, let's take a look at the code around the assertion on StackScriptFunction::BoxState::Box in lib/Runtime/Library/StackScriptFunction.cpp.

247     StackScriptFunction *stackFunction = interpreterFrame->GetStackNestedFunction(i);  
248     ScriptFunction *boxedFunction = this->BoxStackFunction(stackFunction);  
249     Assert(stackFunction->boxedScriptFunction == boxedFunction);  
250     this->UpdateFrameDisplay(stackFunction);

In the above code, it tries to box a stackFunction for some reason, and check whether or not the function is boxed through the assertion. However, the reached assertion shows that it is not really boxed. With gdb, we checked the boxedFunction still indicates stackFunction on the stack, and boxedScriptFunction is nullptr. In normal case, it should points theboxedFunction.

Stopped reason: SIGILL
0x0000555558a9be2f in Js::StackScriptFunction::BoxState::Box (this=0x7ffffffe2540) at /home/soyeon/jsfuzz/js-static/engines/chakracore-1.11.5/lib/Runtime/Library/StackScriptFunction.cpp:249
249                             Assert(stackFunction->boxedScriptFunction == boxedFunction);
$ print boxedFunction
$1 = (Js::ScriptFunction *) 0x7ff7f024fff8
$ print stackFunction
$2 = (Js::StackScriptFunction *) 0x7ff7f024fff8
$ print stackFunction->boxedScriptFunction
$3 = (Js::ScriptFunction *) 0x0

Something went wrong! We should check what really happened in BoxStackFunction.

Why boxing stack function is failed

This is the code snippet of the StackScriptFunction::BoxState::BoxStackFunction.

710     ScriptFunction * StackScriptFunction::BoxState::BoxStackFunction(ScriptFunction * scriptFunction)
711     {
712         // Box the frame display first, which may in turn box the function
713         FrameDisplay * frameDisplay = scriptFunction->GetEnvironment();
714         FrameDisplay * boxedFrameDisplay = BoxFrameDisplay(frameDisplay);
715
716         if (!ThreadContext::IsOnStack(scriptFunction))
717         {
718             return scriptFunction;
719         }
                                    ...
748         boxedFunction = ScriptFunction::OP_NewScFunc(boxedFrameDisplay,
749             reinterpret_cast<FunctionInfoPtrPtr>(&functionInfo));
750         stackFunction->boxedScriptFunction = boxedFunction;

If the scriptFunction is not on the stack, the function does not box the scriptFunction, but just returns the scriptFunction. Probably, this is because the BoxStackFunction wants to avoid boxing a scriptFunction again, which is already boxed and does not exist on the stack. However, it should be located on the stack, as it was StackScriptFunction. This made us suspect the process of the stack variable allocation.

We found a hint in lib/Runtime/Language/InterpreterStackFrame.cpp: Var InterpreterStackFrame::InterpreterHelper.

if (varAllocCount > InterpreterStackFrame::LocalsThreshold)  

While allocating the stack for a function, the engine first checks if the space of the local variables exceeds the threshold (InterpreterStackFrame::LocalsThreshold). If so, the engine will allocate a private arena as the stack instead of using the existing native stack. However, the scope analysis which is done by ThreadContext::IsOnStack afterward, forgets to treat this private arena as a stack frame as well. Therefore, the stack function on the private arena will not be boxed and escape the original scope.

After the function, which has a private arena as its stack, is destructed, the stack will be also un-mapped. However, the non-boxed function still points to the stale stack space, which eventually results in a use-after-unmap vulnerability.

Patch analysis

This is the patch for CVE-2019-0609 released in ChakraCore 1.11.7.

+    if (stackVarAllocCount != 0)
+    {
+    size_t stackVarSizeInBytes = stackVarAllocCount * sizeof(Var);
+    PROBE_STACK_PARTIAL_INITIALIZED_INTERPRETER_FRAME(GetScriptContext(), Js::Constants::MinStackInterpreter + stackVarSizeInBytes);
+    stackAllocation = (Var*)_alloca(stackVarSizeInBytes);
+    }

In the patch, the engine first calculates stackVarAllocCount as the number of stackScriptFunction whether it is necessary to box or not. Then, it moves the stackScriptFunctions to the heap through _alloca.

Proof-of-Concept

Here is the PoC of CVE-2019-0609. The [big-size object] should be a large object literal that has an enough number of initialized members to exceed the threshold to allocate a private arena as the function stack.

function test() {
    function a() {
        function d() {
          let e = function() {};
          return e;
        }
        function b() {
            let fun_d = [d];
            return fun_d;
        }
        var obj = [big-size object]
        return b();
    }
    return a();
}
var f = test();
function test1() {
    var obj = [big-size object] // reallocate for use-after-unmap.
    print(f[0]); // function d still points the address on stack as it is not boxed.
}
test1();