Bug 1081277 - OdinMonkey: allow change heap after detachment in FFI (r=bbouvier)
authorLuke Wagner <luke@mozilla.com>
Tue, 14 Oct 2014 11:03:14 -0500
changeset 210618 62185aa0fd863e426eadac63dbcfaf73e2fec2e4
parent 210617 20aa7722f330ac3a58c418fa2ebfb969ca4b7847
child 210619 397f83283b72e7b74cb4574904fbf94fac34ef25
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersbbouvier
bugs1081277
milestone36.0a1
Bug 1081277 - OdinMonkey: allow change heap after detachment in FFI (r=bbouvier)
js/src/asmjs/AsmJSModule.cpp
js/src/asmjs/AsmJSModule.h
js/src/asmjs/AsmJSValidate.cpp
js/src/jit-test/tests/asm.js/testNeuter.js
js/src/jit-test/tests/asm.js/testProfiling.js
js/src/jit/shared/Assembler-shared.h
--- a/js/src/asmjs/AsmJSModule.cpp
+++ b/js/src/asmjs/AsmJSModule.cpp
@@ -479,16 +479,24 @@ AsmJSModule::setAutoFlushICacheRange()
 
 static void
 AsmJSReportOverRecursed()
 {
     JSContext *cx = PerThreadData::innermostAsmJSActivation()->cx();
     js_ReportOverRecursed(cx);
 }
 
+static void
+OnDetached()
+{
+    // See hasDetachedHeap comment in LinkAsmJS.
+    JSContext *cx = PerThreadData::innermostAsmJSActivation()->cx();
+    JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_OUT_OF_MEMORY);
+}
+
 static bool
 AsmJSHandleExecutionInterrupt()
 {
     AsmJSActivation *act = PerThreadData::innermostAsmJSActivation();
     act->module().setInterrupted(true);
     bool ret = HandleExecutionInterrupt(act->cx());
     act->module().setInterrupted(false);
     return ret;
@@ -662,16 +670,18 @@ AddressOf(AsmJSImmKind kind, ExclusiveCo
       case AsmJSImm_Runtime:
         return cx->runtimeAddressForJit();
       case AsmJSImm_RuntimeInterrupt:
         return cx->runtimeAddressOfInterrupt();
       case AsmJSImm_StackLimit:
         return cx->stackLimitAddressForJitCode(StackForUntrustedScript);
       case AsmJSImm_ReportOverRecursed:
         return RedirectCall(FuncCast(AsmJSReportOverRecursed), Args_General0);
+      case AsmJSImm_OnDetached:
+        return RedirectCall(FuncCast(OnDetached), Args_General0);
       case AsmJSImm_HandleExecutionInterrupt:
         return RedirectCall(FuncCast(AsmJSHandleExecutionInterrupt), Args_General0);
       case AsmJSImm_InvokeFromAsmJS_Ignore:
         return RedirectCall(FuncCast(InvokeFromAsmJS_Ignore), Args_General3);
       case AsmJSImm_InvokeFromAsmJS_ToInt32:
         return RedirectCall(FuncCast(InvokeFromAsmJS_ToInt32), Args_General3);
       case AsmJSImm_InvokeFromAsmJS_ToNumber:
         return RedirectCall(FuncCast(InvokeFromAsmJS_ToNumber), Args_General3);
@@ -875,35 +885,43 @@ AsmJSModule::restoreToInitialState(Array
 }
 
 bool
 AsmJSModule::detachHeap(JSContext *cx)
 {
     MOZ_ASSERT(isDynamicallyLinked());
     MOZ_ASSERT(maybeHeap_);
 
+    // Content JS should not be able to run (and detach heap) from within an
+    // interrupt callback, but in case it does, fail. Otherwise, the heap can
+    // change at an arbitrary instruction and break the assumption below.
+    if (interrupted_) {
+        JS_ReportError(cx, "attempt to detach from inside interrupt handler");
+        return false;
+    }
+
+    // Even if this->active(), to reach here, the activation must have called
+    // out via an FFI stub. FFI stubs check if heapDatum() is null on reentry
+    // and throw an exception if so.
+    MOZ_ASSERT_IF(active(), activation()->exitReason() == AsmJSExit::Reason_IonFFI ||
+                            activation()->exitReason() == AsmJSExit::Reason_SlowFFI);
+
     AutoUnprotectCode auc(cx, *this);
     restoreHeapToInitialState(maybeHeap_);
 
     MOZ_ASSERT(hasDetachedHeap());
     return true;
 }
 
 bool
 js::OnDetachAsmJSArrayBuffer(JSContext *cx, Handle<ArrayBufferObject*> buffer)
 {
     for (AsmJSModule *m = cx->runtime()->linkedAsmJSModules; m; m = m->nextLinked()) {
-        if (buffer == m->maybeHeapBufferObject()) {
-            if (m->active()) {
-                JS_ReportErrorNumber(cx, js_GetErrorMessage, nullptr, JSMSG_OUT_OF_MEMORY);
-                return false;
-            }
-            if (!m->detachHeap(cx))
-                return false;
-        }
+        if (buffer == m->maybeHeapBufferObject() && !m->detachHeap(cx))
+            return false;
     }
     return true;
 }
 
 static void
 AsmJSModuleObject_finalize(FreeOp *fop, JSObject *obj)
 {
     fop->delete_(&obj->as<AsmJSModuleObject>().module());
--- a/js/src/asmjs/AsmJSModule.h
+++ b/js/src/asmjs/AsmJSModule.h
@@ -1049,16 +1049,20 @@ class AsmJSModule
         MOZ_ASSERT(!isFinishedWithModulePrologue());
         pod.funcPtrTableAndExitBytes_ = 0;
         MOZ_ASSERT(isFinishedWithModulePrologue());
     }
 
     /*************************************************************************/
     // These functions are called while parsing/compiling function bodies:
 
+    bool hasArrayView() const {
+        MOZ_ASSERT(isFinishedWithModulePrologue());
+        return pod.hasArrayView_;
+    }
     void addChangeHeap(uint32_t mask, uint32_t min, uint32_t max) {
         MOZ_ASSERT(isFinishedWithModulePrologue());
         MOZ_ASSERT(!pod.hasFixedMinHeapLength_);
         MOZ_ASSERT(IsValidAsmJSHeapLength(mask + 1));
         MOZ_ASSERT(min >= RoundUpToNextValidAsmJSHeapLength(0));
         MOZ_ASSERT(max <= pod.maxHeapLength_);
         MOZ_ASSERT(min <= max);
         pod.heapLengthMask_ = mask;
@@ -1238,20 +1242,16 @@ class AsmJSModule
     bool finish(ExclusiveContext *cx,
                 frontend::TokenStream &tokenStream,
                 jit::MacroAssembler &masm,
                 const jit::Label &interruptLabel);
 
     /*************************************************************************/
     // These accessor functions can be used after finish():
 
-    bool hasArrayView() const {
-        MOZ_ASSERT(isFinished());
-        return pod.hasArrayView_;
-    }
     unsigned numFFIs() const {
         MOZ_ASSERT(isFinished());
         return pod.numFFIs_;
     }
     uint32_t srcEndBeforeCurly() const {
         MOZ_ASSERT(isFinished());
         return srcStart_ + pod.srcLength_;
     }
--- a/js/src/asmjs/AsmJSValidate.cpp
+++ b/js/src/asmjs/AsmJSValidate.cpp
@@ -1324,16 +1324,17 @@ class MOZ_STACK_CLASS ModuleCompiler
     FuncPtrTableVector             funcPtrTables_;
     ArrayViewVector                arrayViews_;
     ExitMap                        exits_;
     MathNameMap                    standardLibraryMathNames_;
     SimdOperationNameMap           standardLibrarySimdOpNames_;
     NonAssertingLabel              stackOverflowLabel_;
     NonAssertingLabel              asyncInterruptLabel_;
     NonAssertingLabel              syncInterruptLabel_;
+    NonAssertingLabel              onDetachedLabel_;
 
     UniquePtr<char[], JS::FreePolicy> errorString_;
     uint32_t                       errorOffset_;
     bool                           errorOverRecursed_;
 
     int64_t                        usecBefore_;
     SlowFunctionVector             slowFunctions_;
 
@@ -1517,16 +1518,17 @@ class MOZ_STACK_CLASS ModuleCompiler
 
     ExclusiveContext *cx() const { return cx_; }
     AsmJSParser &parser() const { return parser_; }
     TokenStream &tokenStream() const { return parser_.tokenStream; }
     MacroAssembler &masm() { return masm_; }
     Label &stackOverflowLabel() { return stackOverflowLabel_; }
     Label &asyncInterruptLabel() { return asyncInterruptLabel_; }
     Label &syncInterruptLabel() { return syncInterruptLabel_; }
+    Label &onDetachedLabel() { return onDetachedLabel_; }
     bool hasError() const { return errorString_ != nullptr; }
     const AsmJSModule &module() const { return *module_.get(); }
     bool usesSignalHandlersForInterrupt() const { return module_->usesSignalHandlersForInterrupt(); }
     bool usesSignalHandlersForOOB() const { return module_->usesSignalHandlersForOOB(); }
     bool supportsSimd() const { return supportsSimd_; }
 
     ParseNode *moduleFunctionNode() const { return moduleFunctionNode_; }
     PropertyName *moduleFunctionName() const { return moduleFunctionName_; }
@@ -7633,16 +7635,37 @@ FillArgumentArray(ModuleCompiler &m, con
                 masm.canonicalizeDouble(ScratchDoubleReg);
                 masm.storeDouble(ScratchDoubleReg, dstAddr);
             }
             break;
         }
     }
 }
 
+// If an FFI detaches its heap (viz., via ArrayBuffer.transfer), it must
+// call change-heap to another heap (viz., the new heap returned by transfer)
+// before returning to asm.js code. If the application fails to do this (if the
+// heap pointer is null), jump to a stub.
+static void
+GenerateCheckForHeapDetachment(ModuleCompiler &m, Register scratch)
+{
+    if (!m.module().hasArrayView())
+        return;
+
+    MacroAssembler &masm = m.masm();
+    AssertStackAlignment(masm, ABIStackAlignment);
+#if defined(JS_CODEGEN_X86)
+    CodeOffsetLabel label = masm.movlWithPatch(PatchedAbsoluteAddress(), scratch);
+    masm.append(AsmJSGlobalAccess(label, AsmJSHeapGlobalDataOffset));
+    masm.branchTestPtr(Assembler::Zero, scratch, scratch, &m.onDetachedLabel());
+#else
+    masm.branchTestPtr(Assembler::Zero, HeapReg, HeapReg, &m.onDetachedLabel());
+#endif
+}
+
 static bool
 GenerateFFIInterpExit(ModuleCompiler &m, const ModuleCompiler::ExitDescriptor &exit,
                       unsigned exitIndex, Label *throwLabel)
 {
     MacroAssembler &masm = m.masm();
     MOZ_ASSERT(masm.framePushed() == 0);
 
     // Argument types for InvokeFromAsmJS_*:
@@ -7716,18 +7739,20 @@ GenerateFFIInterpExit(ModuleCompiler &m,
         break;
       case RetType::Float:
         MOZ_CRASH("Float32 shouldn't be returned from a FFI");
       case RetType::Int32x4:
       case RetType::Float32x4:
         MOZ_CRASH("SIMD types shouldn't be returned from a FFI");
     }
 
-    // The heap pointer may have changed during the FFI, so reload it.
+    // The heap pointer may have changed during the FFI, so reload it and test
+    // for detachment.
     masm.loadAsmJSHeapRegisterFromGlobalData();
+    GenerateCheckForHeapDetachment(m, ABIArgGenerator::NonReturn_VolatileReg0);
 
     Label profilingReturn;
     GenerateAsmJSExitEpilogue(masm, framePushed, AsmJSExit::SlowFFI, &profilingReturn);
     return m.finishGeneratingInterpExit(exitIndex, &begin, &profilingReturn) && !masm.oom();
 }
 
 // On ARM/MIPS, we need to include an extra word of space at the top of the
 // stack so we can explicitly store the return address before making the call
@@ -7926,24 +7951,28 @@ GenerateFFIIonExit(ModuleCompiler &m, co
         MOZ_CRASH("SIMD types shouldn't be returned from a FFI");
     }
 
     Label done;
     masm.bind(&done);
 
     MOZ_ASSERT(masm.framePushed() == framePushed);
 
-    // Reload pinned registers after all calls into arbitrary JS.
+    // Reload the global register since Ion code can clobber any register.
 #if defined(JS_CODEGEN_ARM) || defined(JS_CODEGEN_MIPS)
     JS_STATIC_ASSERT(MaybeSavedGlobalReg > 0);
     masm.loadPtr(Address(StackPointer, savedGlobalOffset), GlobalReg);
 #else
     JS_STATIC_ASSERT(MaybeSavedGlobalReg == 0);
 #endif
+
+    // The heap pointer has to be reloaded anyway since Ion could have clobbered
+    // it. Additionally, the FFI may have detached the heap buffer.
     masm.loadAsmJSHeapRegisterFromGlobalData();
+    GenerateCheckForHeapDetachment(m, ABIArgGenerator::NonReturn_VolatileReg0);
 
     Label profilingReturn;
     GenerateAsmJSExitEpilogue(masm, framePushed, AsmJSExit::IonFFI, &profilingReturn);
 
     if (oolConvert.used()) {
         masm.bind(&oolConvert);
         masm.setFramePushed(framePushed);
 
@@ -8094,16 +8123,30 @@ GenerateBuiltinThunk(ModuleCompiler &m, 
 static bool
 GenerateStackOverflowExit(ModuleCompiler &m, Label *throwLabel)
 {
     MacroAssembler &masm = m.masm();
     GenerateAsmJSStackOverflowExit(masm, &m.stackOverflowLabel(), throwLabel);
     return m.finishGeneratingInlineStub(&m.stackOverflowLabel()) && !masm.oom();
 }
 
+static bool
+GenerateOnDetachedLabelExit(ModuleCompiler &m, Label *throwLabel)
+{
+    MacroAssembler &masm = m.masm();
+    masm.bind(&m.onDetachedLabel());
+    masm.assertStackAlignment(ABIStackAlignment);
+
+    // For now, OnDetached always throws (see OnDetached comment).
+    masm.call(AsmJSImmPtr(AsmJSImm_OnDetached));
+    masm.jump(throwLabel);
+
+    return m.finishGeneratingInlineStub(&m.onDetachedLabel()) && !masm.oom();
+}
+
 static const RegisterSet AllRegsExceptSP =
     RegisterSet(GeneralRegisterSet(Registers::AllMask &
                                    ~(uint32_t(1) << Registers::StackPointer)),
                 FloatRegisterSet(FloatRegisters::AllDoubleMask));
 
 // The async interrupt-callback exit is called from arbitrarily-interrupted asm.js
 // code. That means we must first save *all* registers and restore *all*
 // registers (except the stack pointer) when we resume. The address to resume to
@@ -8147,17 +8190,16 @@ GenerateAsyncInterruptExit(ModuleCompile
 
     masm.branchIfFalseBool(ReturnReg, throwLabel);
 
     // Restore the StackPointer to it's position before the call.
     masm.mov(ABIArgGenerator::NonVolatileReg, StackPointer);
 
     // Restore the machine state to before the interrupt.
     masm.PopRegsInMask(AllRegsExceptSP, AllRegsExceptSP.fpus()); // restore all GP/FP registers (except SP)
-    masm.loadAsmJSHeapRegisterFromGlobalData();  // In case there was a changeHeap
     masm.popFlags();              // after this, nothing that sets conditions
     masm.ret();                   // pop resumePC into PC
 #elif defined(JS_CODEGEN_MIPS)
     // Reserve space to store resumePC.
     masm.subPtr(Imm32(sizeof(intptr_t)), StackPointer);
     // set to zero so we can use masm.framePushed() below.
     masm.setFramePushed(0);
     // When this platform supports SIMD extensions, we'll need to push high lanes
@@ -8189,17 +8231,16 @@ GenerateAsyncInterruptExit(ModuleCompile
     // This will restore stack to the address before the call.
     masm.movePtr(s0, StackPointer);
     masm.PopRegsInMask(AllRegsExceptSP);
 
     // Pop resumePC into PC. Clobber HeapReg to make the jump and restore it
     // during jump delay slot.
     masm.pop(HeapReg);
     masm.as_jr(HeapReg);
-    masm.loadAsmJSHeapRegisterFromGlobalData();  // In case there was a changeHeap
 #elif defined(JS_CODEGEN_ARM)
     masm.setFramePushed(0);         // set to zero so we can use masm.framePushed() below
     masm.PushRegsInMask(RegisterSet(GeneralRegisterSet(Registers::AllMask & ~(1<<Registers::sp)), FloatRegisterSet(uint32_t(0))));   // save all GP registers,excep sp
 
     // Save both the APSR and FPSCR in non-volatile registers.
     masm.as_mrs(r4);
     masm.as_vmrs(r5);
     // Save the stack pointer in a non-volatile register.
@@ -8239,17 +8280,16 @@ GenerateAsyncInterruptExit(ModuleCompile
     masm.transferReg(r7);
     masm.transferReg(r8);
     masm.transferReg(r9);
     masm.transferReg(r10);
     masm.transferReg(r11);
     masm.transferReg(r12);
     masm.transferReg(lr);
     masm.finishDataTransfer();
-    masm.loadAsmJSHeapRegisterFromGlobalData();  // In case there was a changeHeap
     masm.ret();
 
 #elif defined (JS_CODEGEN_NONE)
     MOZ_CRASH();
 #else
 # error "Unknown architecture!"
 #endif
 
@@ -8265,19 +8305,16 @@ GenerateSyncInterruptExit(ModuleCompiler
     unsigned framePushed = StackDecrementForCall(masm, ABIStackAlignment, ShadowStackSpace);
 
     GenerateAsmJSExitPrologue(masm, framePushed, AsmJSExit::Interrupt, &m.syncInterruptLabel());
 
     AssertStackAlignment(masm, ABIStackAlignment);
     masm.call(AsmJSImmPtr(AsmJSImm_HandleExecutionInterrupt));
     masm.branchIfFalseBool(ReturnReg, throwLabel);
 
-    // Reload the heap register in case the callback changed heaps.
-    masm.loadAsmJSHeapRegisterFromGlobalData();
-
     Label profilingReturn;
     GenerateAsmJSExitEpilogue(masm, framePushed, AsmJSExit::Interrupt, &profilingReturn);
     return m.finishGeneratingInterrupt(&m.syncInterruptLabel(), &profilingReturn) && !masm.oom();
 }
 
 // If an exception is thrown, simply pop all frames (since asm.js does not
 // contain try/catch). To do this:
 //  1. Restore 'sp' to it's value right after the PushRegsInMask in GenerateEntry.
@@ -8324,16 +8361,19 @@ GenerateStubs(ModuleCompiler &m)
     for (ModuleCompiler::ExitMap::Range r = m.allExits(); !r.empty(); r.popFront()) {
         if (!GenerateFFIExits(m, r.front().key(), r.front().value(), &throwLabel))
             return false;
     }
 
     if (m.stackOverflowLabel().used() && !GenerateStackOverflowExit(m, &throwLabel))
         return false;
 
+    if (m.onDetachedLabel().used() && !GenerateOnDetachedLabelExit(m, &throwLabel))
+        return false;
+
     if (!GenerateAsyncInterruptExit(m, &throwLabel))
         return false;
     if (m.syncInterruptLabel().used() && !GenerateSyncInterruptExit(m, &throwLabel))
         return false;
 
     if (!GenerateThrowStub(m, &throwLabel))
         return false;
 
--- a/js/src/jit-test/tests/asm.js/testNeuter.js
+++ b/js/src/jit-test/tests/asm.js/testNeuter.js
@@ -1,81 +1,113 @@
 load(libdir + "asm.js");
+load(libdir + "asserts.js");
+
+if (!isAsmJSCompilationAvailable())
+    quit();
 
-function f(stdlib, foreign, buffer) {
-    "use asm";
-    var i32 = new stdlib.Int32Array(buffer);
-    function set(i,j) {
-        i=i|0;
-        j=j|0;
-        i32[i>>2] = j;
-    }
-    function get(i) {
-        i=i|0;
-        return i32[i>>2]|0
-    }
-    return {get:get, set:set}
-}
-if (isAsmJSCompilationAvailable())
-    assertEq(isAsmJSModule(f), true);
+var m = asmCompile('stdlib', 'foreign', 'buffer',
+                  `"use asm";
+                   var i32 = new stdlib.Int32Array(buffer);
+                   function set(i,j) {
+                       i=i|0;
+                       j=j|0;
+                       i32[i>>2] = j;
+                   }
+                   function get(i) {
+                       i=i|0;
+                       return i32[i>>2]|0
+                   }
+                   return {get:get, set:set}`);
 
-var i32 = new Int32Array(1024);
-var buffer = i32.buffer;
-var {get, set} = f(this, null, buffer);
-if (isAsmJSCompilationAvailable())
-    assertEq(isAsmJSFunction(get) && isAsmJSFunction(set), true);
-
+var buffer = new ArrayBuffer(BUF_MIN);
+var {get, set} = asmLink(m, this, null, buffer);
 set(4, 42);
 assertEq(get(4), 42);
-
 neuter(buffer, "change-data");
 neuter(buffer, "same-data");
+assertThrowsInstanceOf(() => get(4), InternalError);
 
-// These operations may throw errors
-try {
-    assertEq(get(4), 0);
-    set(0, 42);
-    assertEq(get(0), 0);
-} catch (e) {
-    assertEq(String(e).indexOf("InternalError"), 0);
-}
+var buf1 = new ArrayBuffer(BUF_MIN);
+var buf2 = new ArrayBuffer(BUF_MIN);
+var {get:get1, set:set1} = asmLink(m, this, null, buf1);
+var {get:get2, set:set2} = asmLink(m, this, null, buf2);
+set1(0, 13);
+set2(0, 42);
+neuter(buf1, "change-data");
+assertThrowsInstanceOf(() => get1(0), InternalError);
+assertEq(get2(0), 42);
 
-function f2(stdlib, foreign, buffer) {
-    "use asm";
-    var i32 = new stdlib.Int32Array(buffer);
-    var ffi = foreign.ffi;
-    function inner(i) {
-        i=i|0;
-        ffi();
-        return i32[i>>2]|0
-    }
-    return inner
-}
-if (isAsmJSCompilationAvailable())
-    assertEq(isAsmJSModule(f2), true);
+var m = asmCompile('stdlib', 'foreign', 'buffer',
+                  `"use asm";
+                   var i32 = new stdlib.Int32Array(buffer);
+                   var ffi = foreign.ffi;
+                   function inner(i) {
+                       i=i|0;
+                       ffi();
+                       return i32[i>>2]|0
+                   }
+                   return inner`);
+
+var buffer = new ArrayBuffer(BUF_MIN);
+function ffi1() { neuter(buffer, "change-data"); }
+var inner = asmLink(m, this, {ffi:ffi1}, buffer);
+assertThrowsInstanceOf(() => inner(8), InternalError);
 
-var i32 = new Int32Array(1024);
-var buffer = i32.buffer;
-var threw = false;
-function ffi() {
-    try {
-        neuter(buffer, "same-data");
-    } catch (e) {
-        assertEq(String(e).indexOf("InternalError"), 0);
-        threw = true;
-    }
-    try {
-        neuter(buffer, "change-data");
-    } catch (e) {
-        assertEq(String(e).indexOf("InternalError"), 0);
-        threw = true;
-    }
+var byteLength = Function.prototype.call.bind(Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength').get);
+var m = asmCompile('stdlib', 'foreign', 'buffer',
+                  `"use asm";
+                   var ffi = foreign.ffi;
+                   var I32 = stdlib.Int32Array;
+                   var i32 = new I32(buffer);
+                   var len = stdlib.byteLength;
+                   function changeHeap(newBuffer) {
+                       if (len(newBuffer) & 0xffffff || len(newBuffer) <= 0xffffff || len(newBuffer) > 0x80000000)
+                           return false;
+                       i32 = new I32(newBuffer);
+                       buffer = newBuffer;
+                       return true;
+                   }
+                   function get(i) {
+                       i=i|0;
+                       return i32[i>>2]|0;
+                   }
+                   function inner(i) {
+                       i=i|0;
+                       ffi();
+                       return get(i)|0;
+                   }
+                   return {changeHeap:changeHeap, get:get, inner:inner}`);
+
+var buf1 = new ArrayBuffer(BUF_CHANGE_MIN);
+var buf2 = new ArrayBuffer(BUF_CHANGE_MIN);
+var buf3 = new ArrayBuffer(BUF_CHANGE_MIN);
+var buf4 = new ArrayBuffer(BUF_CHANGE_MIN);
+new Int32Array(buf2)[13] = 42;
+new Int32Array(buf3)[13] = 1024;
+new Int32Array(buf4)[13] = 1337;
+
+function ffi2() { neuter(buf1, "change-data"); assertEq(changeHeap(buf2), true); }
+var {changeHeap, get:get2, inner} = asmLink(m, this, {ffi:ffi2}, buf1);
+assertEq(inner(13*4), 42);
+
+function ffi3() {
+    assertEq(get2(13*4), 42);
+    assertEq(get2(BUF_CHANGE_MIN), 0)
+    assertEq(get3(13*4), 42);
+    assertEq(get3(BUF_CHANGE_MIN), 0)
+    neuter(buf2, "change-data");
+    assertThrowsInstanceOf(()=>get2(13*4), InternalError);
+    assertThrowsInstanceOf(()=>get2(BUF_CHANGE_MIN), InternalError);
+    assertThrowsInstanceOf(()=>get3(13*4), InternalError);
+    assertThrowsInstanceOf(()=>get3(BUF_CHANGE_MIN), InternalError);
+    assertEq(changeHeap(buf3), true);
+    assertThrowsInstanceOf(()=>get2(13*4), InternalError);
+    assertThrowsInstanceOf(()=>get2(BUF_CHANGE_MIN), InternalError);
+    assertEq(get3(13*4), 1024);
+    assertEq(get3(BUF_CHANGE_MIN), 0);
+    assertEq(changeHeap(buf4), true);
 }
-var inner = f2(this, {ffi:ffi}, buffer);
-if (isAsmJSCompilationAvailable())
-    assertEq(isAsmJSFunction(inner), true);
-i32[2] = 13;
-var result = inner(8);
-if (threw)
-    assertEq(result, 13);
-else
-    assertEq(result, 0);
-
+var {changeHeap, get:get3, inner} = asmLink(m, this, {ffi:ffi3}, buf2);
+assertEq(inner(13*4), 1337);
+assertThrowsInstanceOf(()=>get2(0), InternalError);
+assertEq(get3(BUF_CHANGE_MIN), 0);
+assertEq(get3(13*4), 1337);
--- a/js/src/jit-test/tests/asm.js/testProfiling.js
+++ b/js/src/jit-test/tests/asm.js/testProfiling.js
@@ -1,9 +1,10 @@
 load(libdir + "asm.js");
+load(libdir + "asserts.js");
 
 // Single-step profiling currently only works in the ARM simulator
 if (!getBuildConfiguration()["arm-simulator"])
     quit();
 
 function assertEqualStacks(got, expect)
 {
     // Strip off the " (script/library info)"
@@ -119,16 +120,25 @@ assertEqualStacks(stacks, ",>,f1>,<f1>,>
 // Ion FFI exit
 for (var i = 0; i < 20; i++)
     assertEq(f1(), 32);
 enableSingleStepProfiling();
 assertEq(f1(), 32);
 var stacks = disableSingleStepProfiling();
 assertEqualStacks(stacks, ",>,f1>,<f1>,><f1>,f2><f1>,<f2><f1>,f2><f1>,><f1>,<f1>,f1>,>,");
 
+// Detachment exit
+var buf = new ArrayBuffer(BUF_CHANGE_MIN);
+var ffi = function() { neuter(buf, 'change-data') }
+var f = asmLink(asmCompile('g','ffis','buf', USE_ASM + 'var ffi = ffis.ffi; var i32 = new g.Int32Array(buf); function f() { ffi() } return f'), this, {ffi:ffi}, buf);
+enableSingleStepProfiling();
+assertThrowsInstanceOf(f, InternalError);
+var stacks = disableSingleStepProfiling();
+assertEqualStacks(stacks, ",>,f>,<f>,inline stubf>,<f>,inline stubf>,");
+
 // This takes forever to run.
 // Stack-overflow exit test
 //var limit = -1;
 //var maxct = 0;
 //function ffi(ct) { if (ct == limit) { enableSingleStepProfiling(); print("enabled"); } maxct = ct; }
 //var f = asmLink(asmCompile('g', 'ffis',USE_ASM + "var ffi=ffis.ffi; var ct=0; function rec(){ ct=(ct+1)|0; ffi(ct|0); rec() } function f() { ct=0; rec() } return f"), null, {ffi});
 //// First find the stack limit:
 //var caught = false;
--- a/js/src/jit/shared/Assembler-shared.h
+++ b/js/src/jit/shared/Assembler-shared.h
@@ -762,16 +762,17 @@ enum AsmJSImmKind
     AsmJSImm_ExpD            = AsmJSExit::Builtin_ExpD,
     AsmJSImm_LogD            = AsmJSExit::Builtin_LogD,
     AsmJSImm_PowD            = AsmJSExit::Builtin_PowD,
     AsmJSImm_ATan2D          = AsmJSExit::Builtin_ATan2D,
     AsmJSImm_Runtime,
     AsmJSImm_RuntimeInterrupt,
     AsmJSImm_StackLimit,
     AsmJSImm_ReportOverRecursed,
+    AsmJSImm_OnDetached,
     AsmJSImm_HandleExecutionInterrupt,
     AsmJSImm_InvokeFromAsmJS_Ignore,
     AsmJSImm_InvokeFromAsmJS_ToInt32,
     AsmJSImm_InvokeFromAsmJS_ToNumber,
     AsmJSImm_CoerceInPlace_ToInt32,
     AsmJSImm_CoerceInPlace_ToNumber,
     AsmJSImm_Limit
 };