[Webkit-unassigned] [Bug 139851] New: JSC::VM::m_lastStackTop initialized incorrectly when on non-main fiber; runaway memory corruption

bugzilla-daemon at webkit.org bugzilla-daemon at webkit.org
Sat Dec 20 02:52:53 PST 2014


https://bugs.webkit.org/show_bug.cgi?id=139851

            Bug ID: 139851
           Summary: JSC::VM::m_lastStackTop initialized incorrectly when
                    on non-main fiber; runaway memory corruption
    Classification: Unclassified
           Product: WebKit
           Version: 528+ (Nightly build)
          Hardware: PC
                OS: Windows 8
            Status: NEW
          Severity: Critical
          Priority: P2
         Component: JavaScriptCore
          Assignee: webkit-unassigned at lists.webkit.org
          Reporter: jnlehner at yahoo.com

On Microsoft Windows, if the main thread of an application has fibers, they all share the same thread-local storage.

Now in JSC::Heap::markRoots(), we call sanitizeStackForVM(), which via offlineasm becomes:

sanitizeStackForVMImpl PROC PUBLIC
    mov rdx, qword ptr [51744 + rcx]                         
    cmp rsp, rdx                                             
    jbe _offlineasm_zeroFillDone
    xor rax, rax                                             

  _offlineasm_zeroFillLoop:
    mov qword ptr [0 + rdx], rax                             
    add rdx, 8                                               
    cmp rsp, rdx                                             
    ja _offlineasm_zeroFillLoop

  _offlineasm_zeroFillDone:
    mov rdx, rsp                                             
    mov qword ptr [51744 + rcx], rdx                         
    ret                                                      
sanitizeStackForVMImpl ENDP

qword ptr [51744 + rcx] is the value of JSC::VM::m_lastStackTop.

If we look at all the reads and writes to JSC::VM::m_lastStackTop, besides the read and write above, we find:

WRITES:

JSC::JSLock::didAcquireLock():
JSC::JSLock::grabAllLocks():
    m_vm->setLastStackTop(wtfThreadData().savedLastStackTop());

JSC::VM::VM():
    setLastStackTop(wtfThreadData().stack().origin());

READS:

JSC::JSLock::dropAllLocks():
    wtfThreadData().setSavedLastStackTop(m_vm->lastStackTop());

So the value of JSC::VM::m_lastStackTop depends on what's in the thread-local storage provided by wtfThreadData(), and the only other write to thread-local storage not covered above is:

JSC::initializeThreading():
    std::call_once(initializeThreadingOnceFlag, []{
        // ...
        wtfThreadData().setSavedLastStackTop(wtfThreadData().stack().origin());
    });

There are 2 problems here.  First, JSC::initializeThreading() is only called once for the whole process, so the saved last stack top in thread-local storage will only be for one thread in the process.

But the bigger problem is that JSC::JSLock::didAcquireLock() and JSC::JSLock::grabAllLocks() set JSC::VM::m_lastStackTop based on this value from thread-local storage, even if it is the wrong value when JSC::JSLock::didAcquireLock() or JSC::JSLock::grabAllLocks() are called from another fiber!

Interestingly, note that JSC::VM::VM() sets JSC::VM::m_lastStackTop based on a fresh call to wtfThreadData().stack().origin(), which already has a fix for fiber environments:

 const StackBounds& stack()
    {
        // We need to always get a fresh StackBounds from the OS due to how fibers work.
        // See https://bugs.webkit.org/show_bug.cgi?id=102411
#if OS(WINDOWS)
        m_stackBounds = StackBounds::currentThreadStackBounds();
#endif
        return m_stackBounds;
    }

but this is insufficient to avoid disaster, because JSC::JSLock::didAcquireLock() and JSC::JSLock::grabAllLocks() overwrite the at-construction-time-correct JSC::VM::m_lastStackTop.

Now to explain the result of this problem.  The stack on an Intel machine grows toward lower memory addresses, and Windows threads by default have 1MB of virtual memory reserved for them.  WTF::StackBounds::origin() points to the next byte beyond the end of this allocation:

000000CC`00000000:
guard pages

top of stack

bottom of stack (e.g. main())
000000CC`00100000: origin

sanitizeStackForVMImpl() cares about only two locations in memory: JSC::VM::m_lastStackTop, which is rdx, and rsp. Now it appears that normally, the first time sanitization runs, rdx is supposed to be at the very bottom of the stack, and rsp will be somewhere in the middle, so 

cmp rsp, rdx                                             
jbe _offlineasm_zeroFillDone

executes and we simply update JSC::VM::m_lastStackTop with rsp.  The next time sanitization runs, rsp may have moved closer to the bottom of the stack, so rdx < rsp and we do some zero-filling.

Now suppose we're on a fiber created from the main thread.  JSC::VM::m_lastStackTop will be the origin of the main thread, not the fiber.  If the fiber's stack has been reserved in the virtual address space at a lower address than the main thread, and we're running in the fiber, rsp < rdx, so rdx is then updated to the correct rsp and no zero-filling occurs.

HOWEVER, if the fiber's stack has been reserved in the virtual address space at a higher address than the main thread, now we have rdx < rsp, which to sanitizeStackForVMImpl() looks like everything between must be zero-filled!  I've seen as much as 500MB of difference on Windows 8.1, presumably due to the new high entropy ASLR.

This will show up as a crash report with a stack like:

JavaScriptCore!sanitizeStackForVMImpl(void)+0xf
JavaScriptCore!JSC::Heap::markRoots(double
JavaScriptCore!JSC::Heap::collect(JSC::HeapOperation
JavaScriptCore!JSC::Heap::collectAllGarbage(void)+0x1c
JavaScriptCore!JSC::MarkedAllocator::allocateSlowCase(unsigned
JavaScriptCore!JSC::RegExpPrototype::create(class
JavaScriptCore!JSC::JSGlobalObject::reset(class
JavaScriptCore!JSC::JSGlobalObject::finishCreation(class
JavaScriptCore!JSC::JSGlobalObject::create(class
JavaScriptCore!JSGlobalContextCreateInGroup(struct
...
...
...
kernel32!CreateFiberEx+0x265

if you're lucky, but due to the runaway zeroing of memory, other crashes are possible.

-- 
You are receiving this mail because:
You are the assignee for the bug.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.webkit.org/pipermail/webkit-unassigned/attachments/20141220/e9384234/attachment-0002.html>


More information about the webkit-unassigned mailing list