TurboWarp / scratch-vm

Scratch VM with a JIT compiler and more features
https://turbowarp.org/
Mozilla Public License 2.0
75 stars 72 forks source link

Compiler's waitPromise soft-lock if promise handlers executed immediately #184

Closed FurryR closed 8 months ago

FurryR commented 9 months ago

Expected Behavior

in src/engine/execute.js::handlePromise, L115

const handlePromise = (primitiveReportedValue, sequencer, thread, blockCached, lastOperation) => {
    if (thread.status === Thread.STATUS_RUNNING) {
        // Primitive returned a promise; automatically yield thread.
        thread.status = Thread.STATUS_PROMISE_WAIT; // NOTE: Status is configured **before** registering promise handlers.
    }
    // Promise handlers
    primitiveReportedValue.then(resolvedValue => {
        // ...
    }, rejectionReason => {
        // ...
    });
};

Actual Behavior

in src/compiler/jsexecute.js::isPromise, L131

const isPromise = value => (
    // see engine/execute.js
    value !== null &&
    typeof value === 'object' &&
    typeof value.then === 'function'
); // NOTE: value.catch is not detected. It should not be used either.

in src/compiler/jsexecute.js::waitPromise, L108

const waitPromise = function*(promise) {
    const thread = globalState.thread;
    let returnValue;

    promise
        .then(value => {
            returnValue = value;
            thread.status = 0; // STATUS_RUNNING
        })
        .catch(error => {
            thread.status = 0; // STATUS_RUNNING
            globalState.log.warn('Promise rejected in compiled script:', error);
        }); // NOTE: `catch` might be `undefined`.

    // enter STATUS_PROMISE_WAIT and yield
    // this will stop script execution until the promise handlers reset the thread status
    thread.status = 1; // STATUS_PROMISE_WAIT // NOTE: if promise handlers executed immediately, enter STATUS_PROMISE_WAIT here will cause soft-lock.
    yield;

    return returnValue;
};

Steps to Reproduce

in exploitExtension.js

(function (Scratch) {
  if (!Scratch.extensions.unsandboxed)
      throw new Error('Load this extension as unsandboxed extension');
  class Extension {
      getInfo () {
          return {
              id: 'exploit',
              name: 'Exploit',
              color1: '#000000',
              blocks: [
                  {
                      opcode: 'hack',
                      blockType: Scratch.BlockType.COMMAND,
                      text: 'Exploit'
                  }
              ]
          };
      }
      hack () {
          // returns a PromiseLike that calls handler immediately.
          const pm = {
              then (callbackFn) {
                  callbackFn();
                  return pm;
              },
              catch () {
                  return pm;
              }
          };
          return pm;
      }
  };
  Scratch.extensions.register(new Extension());
})(Scratch);

Operating System and Browser

N/A

FurryR commented 9 months ago

Related issue: https://github.com/scratchfoundation/scratch-vm/issues/4197

FurryR commented 8 months ago

This is resolved by #185.