laverdet / isolated-vm

Secure & isolated JS environments for nodejs
ISC License
2.19k stars 154 forks source link

SegFault on Reference#applySyncPromise #380

Open kiddkai opened 1 year ago

kiddkai commented 1 year ago

Currently, I found when the call stack is deep-ish inside the isolated-vm. There's a chance to trigger a segfaults when main thread(node) finish the job, and the paused isolated-vm thread resume that receive an invalid address to the passed back value.

I tried to replicate this behaviour in a smaller reproducable script but failed so far. I will report what I have for now, will update the thread when I am getting more info.

PID 15324 received SIGSEGV for address: 0x100318e31610
0   segfault-handler.node               0x0000000113291248 _ZL16segfault_handleriP9__siginfoPv + 296
1   libsystem_platform.dylib            0x0000000184f02a24 _sigtramp + 56
2   node                                0x0000000104f3a68c _ZN2v88internal15TransitionArray6GetKeyEi + 28
3   node                                0x0000000105455b28 _ZN2v88internal12BinarySearchILNS0_10SearchModeE0ENS0_15TransitionArrayEEEiPT0_NS0_4NameEiPi + 128
4   node                                0x0000000105450488 _ZN2v88internal15TransitionArray18SearchAndGetTargetENS0_12PropertyKindENS0_4NameENS0_18PropertyAttributesE + 208
5   node                                0x00000001054500b0 _ZN2v88internal19TransitionsAccessor16SearchTransitionENS0_4NameENS0_12PropertyKindENS0_18PropertyAttributesE + 356
6   node                                0x00000001054509c8 _ZN2v88internal19TransitionsAccessor28FindTransitionToDataPropertyENS0_6HandleINS0_4NameEEENS1_17RequestedLocationE + 244
7   node                                0x000000010518e7fc _ZN2v88internal10JsonParserIhE15BuildJsonObjectERKNS2_16JsonContinuationERKNS_4base11SmallVectorINS0_12JsonPropertyELm16EEENS0_6HandleINS0_3MapEEE + 2124
8   node                                0x000000010518abb4 _ZN2v88internal10JsonParserIhE14ParseJsonValueEv + 1692
9   node                                0x0000000105189c18 _ZN2v88internal10JsonParserIhE5ParseEPNS0_7IsolateENS0_6HandleINS0_6StringEEENS5_INS0_6ObjectEEE + 52
10  node                                0x0000000104e31648 _ZN2v88internal17Builtin_JsonParseEiPmPNS0_7IsolateE + 532
11  node                                0x0000000105a2e40c Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit + 108
12  ???                                 0x000000028188e3a4 0x0 + 10763166628
13  ???                                 0x000000028188f1d8 0x0 + 10763170264
14  node                                0x00000001059b4118 Builtins_InterpreterEntryTrampoline + 248
15  node                                0x00000001059b4118 Builtins_InterpreterEntryTrampoline + 248
16  node                                0x00000001059b4118 Builtins_InterpreterEntryTrampoline + 248
17  node                                0x00000001059b18ec Builtins_JSEntryTrampoline + 172
18  node                                0x00000001059b1584 Builtins_JSEntry + 164
19  node                                0x0000000104f610f4 _ZN2v88internal12_GLOBAL__N_16InvokeEPNS0_7IsolateERKNS1_12InvokeParamsE + 2972
20  node                                0x0000000104f60544 _ZN2v88internal9Execution4CallEPNS0_7IsolateENS0_6HandleINS0_6ObjectEEES6_iPS6_ + 100
21  node                                0x0000000104d3b8a8 _ZN2v86Script3RunENS_5LocalINS_7ContextEEE + 1116
22  isolated_vm.node                    0x000000011d886640 _ZZN3ivm10EvalRunner6Phase2EvENKUlvE0_clEv + 56
23  isolated_vm.node                    0x000000011d8858f8 _ZN3ivm14RunWithTimeoutIZNS_10EvalRunner6Phase2EvEUlvE0_EEN2v85LocalINS3_5ValueEEEjOT_ + 284
24  isolated_vm.node                    0x000000011d884764 _ZN3ivm10EvalRunner6Phase2Ev + 204
25  isolated_vm.node                    0x000000011d85fd18 _ZN3ivm14ThreePhaseTask7RunSyncERNS_13IsolateHolderEb + 576
26  isolated_vm.node                    0x000000011d88b084 _ZN3ivm14ThreePhaseTask3RunILi0ENS_10EvalRunnerEJRNS_12RemoteHandleIN2v87ContextEEERNS4_5LocalINS4_6StringEEERNS4_10MaybeLocalINS4_6ObjectEEEEEENS8_INS4_5ValueEEERNS_13IsolateHolderEDpOT1_ + 140
27  isolated_vm.node                    0x000000011d88afa8 _ZN3ivm13ContextHandle4EvalILi0EEEN2v85LocalINS2_5ValueEEENS3_INS2_6StringEEENS2_10MaybeLocalINS2_6ObjectEEE + 76
28  isolated_vm.node                    0x000000011d88b180 _ZN3ivm6detail22unbind_member_functionIMNS_13ContextHandleEFN2v85LocalINS3_5ValueEEENS4_INS3_6StringEEENS3_10MaybeLocalINS3_6ObjectEEEEE6invokeIXadL_ZNS2_4EvalILi0EEES6_S8_SB_EEEES6_RS2_S8_SB_ + 132
29  isolated_vm.node                    0x000000011d88b408 _ZZN3ivm6detail13CallbackMakerIPFN2v85LocalINS2_5ValueEEERNS_13ContextHandleENS3_INS2_6StringEEENS2_10MaybeLocalINS2_6ObjectEEEEXadL_ZNS0_22unbind_member_functionIMS6_FS5_S9_SC_EE6invokeIXadL_ZNS6_4EvalILi0EEES5_S9_SC_EEEES5_S7_S9_SC_EELin1EJRKNS2_20FunctionCallbackInfoIS4_EEEE6SpreadIJLm0ELm1ELm2EEEEvSO_NSt3__116integer_sequenceImJXspT_EEEEENKUlvE_clEv + 132
30  isolated_vm.node                    0x000000011d88b370 _ZN3ivm6detail17InvokeWithoutVoidIZNS0_13CallbackMakerIPFN2v85LocalINS3_5ValueEEERNS_13ContextHandleENS4_INS3_6StringEEENS3_10MaybeLocalINS3_6ObjectEEEEXadL_ZNS0_22unbind_member_functionIMS7_FS6_SA_SD_EE6invokeIXadL_ZNS7_4EvalILi0EEES6_SA_SD_EEEES6_S8_SA_SD_EELin1EJRKNS3_20FunctionCallbackInfoIS5_EEEE6SpreadIJLm0ELm1ELm2EEEEvSP_NSt3__116integer_sequenceImJXspT_EEEEEUlvE_EEDcT_PNSS_9enable_ifIXntsr3std7is_sameIvDTclfL0p_EEEE5valueEvE4typeE + 32
31  isolated_vm.node                    0x000000011d88b334 _ZN3ivm6detail13CallbackMakerIPFN2v85LocalINS2_5ValueEEERNS_13ContextHandleENS3_INS2_6StringEEENS2_10MaybeLocalINS2_6ObjectEEEEXadL_ZNS0_22unbind_member_functionIMS6_FS5_S9_SC_EE6invokeIXadL_ZNS6_4EvalILi0EEES5_S9_SC_EEEES5_S7_S9_SC_EELin1EJRKNS2_20FunctionCallbackInfoIS4_EEEE6SpreadIJLm0ELm1ELm2EEEEvSO_NSt3__116integer_sequenceImJXspT_EEEE + 36
error Command failed with signal "SIGSEGV".

What I am doing with the isolated-vm was:

  1. isolated-vm thread runs entry code, when the entry code hits a require call, make a applySyncPromise to main thread.
  2. main thread returns the resolved dependency info.
  3. isolated-vm thread ask for the compiled code from main thread again also via applySyncPromise.
  4. isolated-vm thread eval the resolved code.

This generates a relative deep call stacks, it also segfault ramdomly. But what I've observed when crashing is that the passed value, currently is string only, the pointer is pointing to an invalid address doesn't contain anything when isolated-vm thread gets the result from syncPromise.


Screenshot in vscode for stacktrace, and the invalid addr on the transfer back value: "should be a json string".

image

It happen randomly from different threads, sometimes main thread, sometimes isolated-vm thread.

When SegFault happen in main thread

A screenshot about the js callstack, breaks while running AsyncCallback:

image

When it happen in the main thread, it seem like the value is passed correctly based on js stack trace:


==== JS stack trace =========================================

    0: ExitFrame [pc: 0x1010ea7f4]
Security context: 0x065838b2a579 <JSGlobalObject>#0#
    1: /* anonymous */(aka /* anonymous */) [0x772d5bcc171] [0x07b633f81599 <undefined>:~1] [pc=0x118179434](this=0x07b633f81599 <undefined>,0x0772d5bdb709 <String[125]: c"{"filePath":".../node_modules/@emotion/react/dist/emotion-react.esm.js","sideEffects":false}">)
    2: StubFrame [pc: 0x1011befe8]
    3: StubFrame [pc: 0x10110b9b4]
    4: EntryFrame [pc: 0x1010e57c4]
    5: ExitFrame [pc: 0x1010ea7f4]
    6: processTicksAndRejections [0x65838b075c1] [node:internal/process/task_queues:~68] [pc=0x118151fdc](this=0x065838b02729 <process map = 0x1e7a0fd41479>#1#)
    7: InternalFrame [pc: 0x1010e58ec]
    8: EntryFrame [pc: 0x1010e5584]

==== Details ================================================

[0]: ExitFrame [pc: 0x1010ea7f4]
[1]: /* anonymous */(aka /* anonymous */) [0x772d5bcc171] [0x07b633f81599 <undefined>:~1] [pc=0x118179434](this=0x07b633f81599 <undefined>,0x0772d5bdb709 <String[125]: c"{"filePath":".../node_modules/@emotion/react/dist/emotion-react.esm.js","sideEffects":false}">) {
// optimized frame
--------- s o u r c e   c o d e ---------
function (val) {AsyncCallback(ptr, did_finish, val);}
-----------------------------------------
}
[2]: StubFrame [pc: 0x1011befe8]
[3]: StubFrame [pc: 0x10110b9b4]
[4]: EntryFrame [pc: 0x1010e57c4]
[5]: ExitFrame [pc: 0x1010ea7f4]
[6]: processTicksAndRejections [0x65838b075c1] [node:internal/process/task_queues:~68] [pc=0x118151fdc](this=0x065838b02729 <process map = 0x1e7a0fd41479>#1#) {
// optimized frame
--------- s o u r c e   c o d e ---------
function processTicksAndRejections() {\x0a  let tock;\x0a  do {\x0a    while ((tock = queue.shift()) !== null) {\x0a      const asyncId = tock[async_id_symbol];\x0a      emitBefore(asyncId, tock[trigger_async_id_symbol], tock);\x0a\x0a      try {\x0a        const callback = tock.callback;\x0a        if (tock.args === undefined) {\x0a          callback();\x0a       ...

-----------------------------------------
}
[7]: InternalFrame [pc: 0x1010e58ec]
[8]: EntryFrame [pc: 0x1010e5584]
-- ObjectCacheKey --

 #0# 0x65838b2a579: 0x065838b2a579 <JSGlobalObject>
 #1# 0x65838b02729: 0x065838b02729 <process map = 0x1e7a0fd41479>
=====================

While broken when trying to unlock:

image

When SegFault happen in isolated-vm thread

Main thread stack trace shows correct value:

Attempt to print stack while printing stack (double fault)
If you are lucky you may find a partial stack dump on stdout.

==== JS stack trace =========================================

Security context: 0x1addd2337b49 <JSGlobalObject>#0#
    0: builtin exit frame: parse(this=0x1addd2327159 <Object map = 0x130babe48349>#1#,0x1addd2327159 <Object map = 0x130babe48349>#1#,0x300dea002bf1 <String[184]: "{"filePath":"..*.js","sideEffects":false}">)

Then in the isolated-vm thread, it gets:

image

Interestingly, the main thread's exit frame seems to run code that should be run in the isolated-vm thread. When the isolated-vm thread received the value, it runs JSON.parse.

kiddkai commented 1 year ago

@laverdet I found a minimum reproducable script to break it.

Currently I put it in the ./tests folder and run it.

'use strict';
let ivm = require('isolated-vm');

const resData = async () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(JSON.stringify({ filePath: '/test', code: 'run();' }));
        }, 10);
    });
};

function makeIsolate() {
    let isolate = new ivm.Isolate;
    let context = isolate.createContextSync();
    let global = context.global;
    return { isolate, context, global };
}

const main = async () => {
    // Basic test.. tests unlocking
    let env = makeIsolate();
    env.global.setSync('ivm', ivm);
    env.global.setSync('ctx', env.context);
    env.global.setSync('fn', new ivm.Reference(async function () {
        const result = await resData();
        return result;
    }));

    env.global.setSync('log', new ivm.Callback(function (...args) {
        console.log(...args);
    }));

    const script = env.isolate.compileScriptSync(`
        counter = 0;
        function run() {
            if (counter > 128) return;
            counter++;
            const res = JSON.parse(fn.applySyncPromise(undefined, []));
            log(counter, res.filePath);
            ctx.evalSync(res.code)
            return '';
        }
        run();
    `);

    for (let i = 0; i < 100; i++) {
        await script.run(env.context);
    }
    console.log('pass');
};

main();

image