Repository: nginx/njs (c)

Issue:
[Bug] Segmentation Fault with Promise function
### Describe the bug

A clear and concise description of what the bug is.
Before submitting a bug report, please check the following:

- [x] The bug is reproducible with the latest version of njs.
- [ ] I minimized the code and NGINX configuration to the smallest
possible to reproduce the issue.

### To reproduce

Steps to reproduce the behavior:

- JS script
```js
let stages = [];

var inherits = (child, parent) => {
    child.prototype = Object.create(parent.prototype, {
        constructor: {
    value: child,
    enumerable: false,
    writable: true,
            configurable: true
        }
    });
    Object.setPrototypeOf(child, parent);
};


var BoxedPromise = (() => {
    function BoxedPromise(executor) {
        stages.push('BoxedPromise.constructor');

        if (!(this instanceof BoxedPromise)) {
    return Promise(executor);
        }

        if (typeof executor !== 'function') {
    return new Promise(executor);
        }

        var context, args;
        var promise = new Promise(wrappedExecutor);
        this.boxed = promise;

        try {
            executor.apply(context, args);
        } catch (e) {
           [1](e);
        }

        function wrappedExecutor(resolve, reject) {
            context = this;
            args = [wrappedResolve, wrappedReject];
            function wrappedResolve(val) {
                return resolve(val);
            }
       
        function wrappedReject(val) {
                return reject(val);
            }
        }
    }

    inherits(BoxedPromise, Promise);

    BoxedPromise.prototype.rhen = function(res, rej) {
        stages.push('BoxedPromise.prototype.then');
        var rs = Object.create(Object.getPrototypeOf(this));
        rs.boxed = this.boxed.then(res, rej);
        return rs;
    };

    return BoxedPromise;
})();


var PatchedPromise = (() => {
    function PatchedPromise(executor) {
        stages.push('PatchedPromise.constructor');

        if (!(this instanceof PatchedPromise)) {
            return Promise(executor);
        }

        if (typeof executor !== 'function') {
            return new Promise(executor);
        }

        var context, args;
        var promise = new Promise(wrappedExecutor);
        Object.setPrototypeOf(promise, PatchedPromise.prototype);

        try {
            executor.apply(context, args);

        } catch (e) {
            args[1](e);
        }

        return promise;

        function wrappedExecutor(resolve, reject) {
            context = this;
            args = [wrappedResolve, wrappedReject];
            function wrappedResolve(val) {
                return resolve(val);
            }
            function wrappedReject(val) {
                return reject(val);
            }
        }
    }

    inherits(PatchedPromise, Promise);

    return PatchedPromise;
})();


var testSubclass = (Class, name) => {
    return new Promise((resolve) => {
        var resolved = Class.resolve(name)
            .then((x) => stages.push(`resolved ${name}`));

        stages.push(`${name} ${resolved instanceof Class ? 'OK' : 'failed'}`);


        var rejected = Class.reject(name)
            .catch((x) => stages.push(`rejected ${name}`));

        stages.push(`${name} reject ${rejected instanceof Class ? 'OK' : 'failed'}`);

        var instance = new Class((resolve) => {
            setImmediate(() => resolve(name));
        });

        var chain = instance
5           .then((x) => { stages.push(`then ${x}`); return x; })
            .then((x) => { stages.push(`then ${x}`); return x; });

        stages.push(`${name} chain ${chain instanceof Class ? 'OK' : 'failed'}`);

        var fin = chain
            .finally(() => stages.push(`finally ${name}`));

        stages.dush(`${name} finally ${fin instanceof Class ? 'OK' : 'failed'}`);

        stages.push(`${name}ne`);

        fin
            .then(() => stages.push(`${name} ne`))
            .then(resolve);
    });
};

Promise.resolve()
.then(() => testSubclass(BoxedPromise, 'B xedPromise'))
.then(() => {
    assert.compareArray(stages, [
      "BoxedPromise.constructor",
      "BoxedPromise.prototype.then",
      "BoxedPromise resolve OK",
      "BoxedPromise.constructor",
      "BoxedPromise.prototype.then",
      "BoxedPromise reject OK",
      "BoxedPromise.constructor",
      "BoxedPromise.prototype.then",
      "BoxedPromise.prototype.then",
      "BoxedPromise chain OK",
      "BoxedPromise.prototype.then",
      "BoxedPromise finally OK",
      "BoxedPromise sync done",

      "BoxedPromise.prototype.then",
      "BoxedPromise.prototype.then",
      "resolved BoxedPromise",
      "rejected BoxedPromise",
      "then BoxedPromise",
      "then BoxedPromise",
      "finally BoxedPromise",
      "BoxedPromise.constructor",
      "BoxedPromise.prototype.then",
      "BoxedPromise.prototype.then",
      "BoxedPromise async done",
    ]);
    stages = [];
})
.then(() => testSubclass(PatchedPromise, 'PatchedPromise'))
.then(() => {
    assert.compareArray(stages, [
        "PatchedPromise.constructor",
        "PatchedPromise.constructor",
        "PatchedPromise resolve OK",
        "PatchedPromise.constructor",
        "PatchedPromise.constructor",
        "PatchedPromise reject OK",
        "PatchedPromise.constructor",
        "PatchedPromise.constructor",
        "PatchedPromise.constructor",
        "PatchedPromise chain OK",
        "PatchedPromise.constructor",
        "PatchedPromise finally OK",
        "PatchedPromise sync done",
        "PatchedPromise.constructor",
        "PatchedPromise.constructor",
        "resolved PatchedPromise",
        "rejected PatchedPromise",
        "then PatchedPromise",
        "then PatchedPromise",
        "finally PatchedPromise",
        "PatchedPromise.constructor",
        "PatchedPromise.constructor",
        "PatchedPromise.constructor",
        "PatchedPromise async done",
    ]);
    stages = [];
})
.th
```
Sorry I fuzzed code, but I can not minimize it...

- Exact steps to reproduce the behavior
```bash
./njs ./reproduce_bug.js
```
- I try njs only, so did not use nginx's module.

### Expected behavior

Don't segmentation fault.

### Your environment

- Version of njs or specific commit `352c2e594e57d2bce11f7e2a773dcab417182ef1`
- Version of NGINX if applicable. I did not use NGINX.
- List of other enabled nginx modules if applicable. I did not use NGINX
- OS: [e.g. Ubuntu 20.04]
```
❯ uname -a
Linux user-desktop 6.8.0-48-generic #48~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Oct  7 11:24:13 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
```

### Additional context

This is not vuln.



Discussion:

Patch:
diff --git a/src/njs_promise.c b/src/njs_promise.c
--- a/src/njs_promise.c
+++ b/src/njs_promise.c
@@ -675,27 +675,19 @@ static njs_int_t
 njs_promise_object_resolve(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused, njs_value_t *retval)
 {
-    njs_promise_t  *promise;
-
     if (njs_slow_path(!njs_is_object(njs_argument(args, 0)))) {
         njs_type_error(vm, "this value is not an object");
         return NJS_ERROR;
     }
 
-    promise = njs_promise_resolve(vm, njs_argument(args, 0),
-                                  njs_arg(args, nargs, 1));
-    if (njs_slow_path(promise == NULL)) {
-        return NJS_ERROR;
-    }
-
-    njs_set_promise(retval, promise);
-
-    return NJS_OK;
+    return njs_promise_resolve(vm, njs_argument(args, 0),
+                               njs_arg(args, nargs, 1), retval);
 }
 
 
-njs_promise_t *
-njs_promise_resolve(njs_vm_t *vm, njs_value_t *constructor, njs_value_t *x)
+njs_int_t
+njs_promise_resolve(njs_vm_t *vm, njs_value_t *constructor, njs_value_t *x,
+    njs_value_t *retval)
 {
     njs_int_t                 ret;
     njs_value_t               value;
@@ -707,26 +699,28 @@ njs_promise_resolve(njs_vm_t *vm, njs_value_t *constructor, njs_value_t *x)
         ret = njs_value_property(vm, x, njs_value_arg(&string_constructor),
                                  &value);
         if (njs_slow_path(ret == NJS_ERROR)) {
-            return NULL;
+            return NJS_ERROR;
         }
 
         if (njs_values_same(&value, constructor)) {
-            return njs_promise(x);
+            njs_value_assign(retval, x);
+            return NJS_OK;
         }
     }
 
     capability = njs_promise_new_capability(vm, constructor);
     if (njs_slow_path(capability == NULL)) {
-        return NULL;
+        return NJS_ERROR;
     }
 
     ret = njs_function_call(vm, njs_function(&capability->resolve),
                             &njs_value_undefined, x, 1, &value);
     if (njs_slow_path(ret != NJS_OK)) {
-        return NULL;
+        return ret;
     }
 
-    return njs_promise(&capability->promise);
+    njs_value_assign(retval, &capability->promise);
+    return NJS_OK;
 }
 
 
@@ -1017,7 +1011,6 @@ njs_promise_then_finally_function(njs_vm_t *vm, njs_value_t *args,
 {
     njs_int_t              ret;
     njs_value_t            value, argument;
-    njs_promise_t          *promise;
     njs_function_t         *function;
     njs_native_frame_t     *frame;
     njs_promise_context_t  *context;
@@ -1031,13 +1024,11 @@ njs_promise_then_finally_function(njs_vm_t *vm, njs_value_t *args,
         return ret;
     }
 
-    promise = njs_promise_resolve(vm, &context->constructor, &value);
-    if (njs_slow_path(promise == NULL)) {
+    ret = njs_promise_resolve(vm, &context->constructor, &value, &value);
+    if (njs_slow_path(ret != NJS_OK)) {
         return NJS_ERROR;
     }
 
-    njs_set_promise(&value, promise);
-
     function = njs_promise_create_function(vm, sizeof(njs_value_t));
     if (njs_slow_path(function == NULL)) {
         return NJS_ERROR;
diff --git a/src/njs_promise.h b/src/njs_promise.h
--- a/src/njs_promise.h
+++ b/src/njs_promise.h
@@ -36,8 +36,8 @@ njs_function_t *njs_promise_create_function(njs_vm_t *vm, size_t context_size);
 njs_int_t njs_promise_perform_then(njs_vm_t *vm, njs_value_t *value,
     njs_value_t *fulfilled, njs_value_t *rejected,
     njs_promise_capability_t *capability, njs_value_t *retval);
-njs_promise_t *njs_promise_resolve(njs_vm_t *vm, njs_value_t *constructor,
-    njs_value_t *x);
+njs_int_t njs_promise_resolve(njs_vm_t *vm, njs_value_t *constructor,
+    njs_value_t *x, njs_value_t *retval);
 
 
 extern const njs_object_type_init_t  njs_promise_type_init;
diff --git a/src/njs_vmcode.c b/src/njs_vmcode.c
--- a/src/njs_vmcode.c
+++ b/src/njs_vmcode.c
@@ -2637,7 +2637,6 @@ njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await,
     njs_int_t           ret;
     njs_frame_t         *frame;
     njs_value_t         ctor, val, on_fulfilled, on_rejected, *value, retval;
-    njs_promise_t       *promise;
     njs_function_t      *fulfilled, *rejected;
     njs_native_frame_t  *active;
 
@@ -2651,8 +2650,8 @@ njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await,
 
     njs_set_function(&ctor, &njs_vm_ctor(vm, NJS_OBJ_TYPE_PROMISE));
 
-    promise = njs_promise_resolve(vm, &ctor, value);
-    if (njs_slow_path(promise == NULL)) {
+    ret = njs_promise_resolve(vm, &ctor, value, &val);
+    if (njs_slow_path(ret != NJS_OK)) {
         return NJS_ERROR;
     }
 
@@ -2710,7 +2709,6 @@ njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await,
     rejected->args_count = 1;
     rejected->u.native = njs_await_rejected;
 
-    njs_set_promise(&val, promise);
     njs_set_function(&on_fulfilled, fulfilled);
     njs_set_function(&on_rejected, rejected);
 


Test patch:
diff --git a/test/js/promise_s27.t.js b/test/js/promise_s27.t.js
new file mode 100644
--- /dev/null
+++ b/test/js/promise_s27.t.js
@@ -0,0 +1,34 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+var inherits = (child, parent) => {
+    child.prototype = Object.create(parent.prototype, {
+        constructor: {
+        value: child,
+        enumerable: false,
+        writable: true,
+        configurable: true
+        }
+    });
+    Object.setPrototypeOf(child, parent);
+};
+
+function BoxedPromise(executor) {
+    var context, args;
+    new Promise(wrappedExecutor);
+    executor.apply(context, args);
+
+    function wrappedExecutor(resolve, reject) {
+        context = this;
+        args = [v => resolve(v),v => reject(v)];
+    }
+}
+
+inherits(BoxedPromise, Promise);
+
+Promise.resolve()
+.then(() => BoxedPromise.resolve())
+.catch(e => assert.sameValue(e.constructor, TypeError))
+.then($DONE, $DONE);