CVE-2022-25139: Use After Free (CWE-416)
value: njs through 0.7.0, used in NGINX, was discovered to contain a heap use-after-free in njs_await_fulfilled.
CWE description: The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
CVSS v3 base score: 9.8
Published: 2022-02-14T22:15Z
Repository: https://github.com/nginx/njs
Language: C
Files changed: src/njs_async.c, src/njs_function.c, src/njs_function.h, src/njs_value.h, src/njs_vm.c, src/njs_vmcode.c, src/njs_vmcode.h, test/js/async_recursive_last.t.js, test/js/async_recursive_mid.t.js
Commit date: 2022-01-21 14:31:30 +0000
Commit message: Fixed recursive async function calls.

Previously, PromiseCapability record was stored (function->context)
directly in function object during a function invocation.  This is
not correct, because PromiseCapability record should be linked to
current execution context.  As a result, function->context is
overwritten with consecutive recursive calls which results in
use-after-free.

This closes #451 issue on Github.

Diff: diff --git a/src/njs_async.c b/src/njs_async.c
index 7bc6c37e..e4dd7486 100644
--- a/src/njs_async.c
+++ b/src/njs_async.c
@@ -27,13 +27,11 @@ njs_async_function_frame_invoke(njs_vm_t *vm, njs_value_t *retval)
     capability = njs_promise_new_capability(vm, &ctor);
     if (njs_slow_path(capability == NULL)) {
         return NJS_ERROR;
     }
 
-    frame->function->context = capability;
-
-    ret = njs_function_lambda_call(vm);
+    ret = njs_function_lambda_call(vm, capability, NULL);
 
     if (ret == NJS_OK) {
         ret = njs_function_call(vm, njs_function(&capability->resolve),
                                 &njs_value_undefined, retval, 1, &vm->retval);
 
@@ -61,11 +59,10 @@ njs_await_fulfilled(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused)
 {
     njs_int_t           ret;
     njs_value_t         **cur_local, **cur_closures, **cur_temp, *value;
     njs_frame_t         *frame, *async_frame;
-    njs_function_t      *function;
     njs_async_ctx_t     *ctx;
     njs_native_frame_t  *top, *async;
 
     ctx = vm->top_frame->function->context;
 
@@ -76,12 +73,10 @@ njs_await_fulfilled(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 
     async_frame = ctx->await;
     async = &async_frame->native;
     async->previous = vm->top_frame;
 
-    function = async->function;
-
     cur_local = vm->levels[NJS_LEVEL_LOCAL];
     cur_closures = vm->levels[NJS_LEVEL_CLOSURE];
     cur_temp = vm->levels[NJS_LEVEL_TEMP];
     top = vm->top_frame;
     frame = vm->active_frame;
@@ -96,17 +91,11 @@ njs_await_fulfilled(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     *njs_scope_value(vm, ctx->index) = *value;
     vm->retval = *value;
 
     vm->top_frame->retval = &vm->retval;
 
-    function->context = ctx->capability;
-    function->await = ctx;
-
-    ret = njs_vmcode_interpreter(vm, ctx->pc);
-
-    function->context = NULL;
-    function->await = NULL;
+    ret = njs_vmcode_interpreter(vm, ctx->pc, ctx->capability, ctx);
 
     vm->levels[NJS_LEVEL_LOCAL] = cur_local;
     vm->levels[NJS_LEVEL_CLOSURE] = cur_closures;
     vm->levels[NJS_LEVEL_TEMP] = cur_temp;
 
diff --git a/src/njs_function.c b/src/njs_function.c
index ae0fa11f..5f07ddd9 100644
--- a/src/njs_function.c
+++ b/src/njs_function.c
@@ -606,11 +606,11 @@ njs_function_call2(njs_vm_t *vm, njs_function_t *function,
     return ret;
 }
 
 
 njs_int_t
-njs_function_lambda_call(njs_vm_t *vm)
+njs_function_lambda_call(njs_vm_t *vm, void *promise_cap, void *async_ctx)
 {
     uint32_t               n;
     njs_int_t              ret;
     njs_frame_t            *frame;
     njs_value_t            *args, **local, *value;
@@ -620,10 +620,12 @@ njs_function_lambda_call(njs_vm_t *vm)
     njs_function_lambda_t  *lambda;
 
     frame = (njs_frame_t *) vm->top_frame;
     function = frame->native.function;
 
+    njs_assert(function->context == NULL);
+
     if (function->global && !function->closure_copied) {
         ret = njs_function_capture_global_closures(vm, function);
         if (njs_slow_path(ret != NJS_OK)) {
             return NJS_ERROR;
         }
@@ -696,11 +698,11 @@ njs_function_lambda_call(njs_vm_t *vm)
         if (njs_slow_path(ret != NJS_OK)) {
             return ret;
         }
     }
 
-    ret = njs_vmcode_interpreter(vm, lambda->start);
+    ret = njs_vmcode_interpreter(vm, lambda->start, promise_cap, async_ctx);
 
     /* Restore current level. */
     vm->levels[NJS_LEVEL_LOCAL] = cur_local;
     vm->levels[NJS_LEVEL_CLOSURE] = cur_closures;
     vm->levels[NJS_LEVEL_TEMP] = cur_temp;
@@ -773,11 +775,11 @@ njs_function_frame_invoke(njs_vm_t *vm, njs_value_t *retval)
 
     if (frame->native) {
         return njs_function_native_call(vm);
 
     } else {
-        return njs_function_lambda_call(vm);
+        return njs_function_lambda_call(vm, NULL, NULL);
     }
 }
 
 
 void
diff --git a/src/njs_function.h b/src/njs_function.h
index b47e7dc6..59150fdd 100644
--- a/src/njs_function.h
+++ b/src/njs_function.h
@@ -110,11 +110,12 @@ njs_int_t njs_function_lambda_frame(njs_vm_t *vm, njs_function_t *function,
     const njs_value_t *this, const njs_value_t *args, njs_uint_t nargs,
     njs_bool_t ctor);
 njs_int_t njs_function_call2(njs_vm_t *vm, njs_function_t *function,
     const njs_value_t *this, const njs_value_t *args,
     njs_uint_t nargs, njs_value_t *retval, njs_bool_t ctor);
-njs_int_t njs_function_lambda_call(njs_vm_t *vm);
+njs_int_t njs_function_lambda_call(njs_vm_t *vm, void *promise_cap,
+    void *async_ctx);
 njs_int_t njs_function_native_call(njs_vm_t *vm);
 njs_native_frame_t *njs_function_frame_alloc(njs_vm_t *vm, size_t size);
 void njs_function_frame_free(njs_vm_t *vm, njs_native_frame_t *frame);
 njs_int_t njs_function_frame_save(njs_vm_t *vm, njs_frame_t *native,
     u_char *pc);
diff --git a/src/njs_value.h b/src/njs_value.h
index 12448eac..7297df39 100644
--- a/src/njs_value.h
+++ b/src/njs_value.h
@@ -268,11 +268,10 @@ struct njs_function_s {
         njs_function_native_t         native;
         njs_function_t                *bound_target;
     } u;
 
     void                              *context;
-    void                              *await;
 
     njs_value_t                       *bound;
 };
 
 
diff --git a/src/njs_vm.c b/src/njs_vm.c
index b46f88c6..0c75fff7 100644
--- a/src/njs_vm.c
+++ b/src/njs_vm.c
@@ -488,11 +488,11 @@ njs_vm_start(njs_vm_t *vm)
     ret = njs_module_load(vm);
     if (njs_slow_path(ret != NJS_OK)) {
         return ret;
     }
 
-    ret = njs_vmcode_interpreter(vm, vm->start);
+    ret = njs_vmcode_interpreter(vm, vm->start, NULL, NULL);
 
     return (ret == NJS_ERROR) ? NJS_ERROR : NJS_OK;
 }
 
 
diff --git a/src/njs_vmcode.c b/src/njs_vmcode.c
index b371c374..3039642c 100644
--- a/src/njs_vmcode.c
+++ b/src/njs_vmcode.c
@@ -40,11 +40,12 @@ static njs_jump_off_t njs_vmcode_typeof(njs_vm_t *vm, njs_value_t *value,
 static njs_jump_off_t njs_vmcode_debugger(njs_vm_t *vm);
 
 static njs_jump_off_t njs_vmcode_return(njs_vm_t *vm, njs_value_t *invld,
     njs_value_t *retval);
 
-static njs_jump_off_t njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await);
+static njs_jump_off_t njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await,
+    njs_promise_capability_t *pcap, njs_async_ctx_t *actx);
 
 static njs_jump_off_t njs_vmcode_try_start(njs_vm_t *vm, njs_value_t *value,
     njs_value_t *offset, u_char *pc);
 static njs_jump_off_t njs_vmcode_try_break(njs_vm_t *vm, njs_value_t *value,
     njs_value_t *offset);
@@ -75,11 +76,12 @@ static njs_jump_off_t njs_function_frame_create(njs_vm_t *vm,
         }                                                                     \
     } while (0)
 
 
 njs_int_t
-njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc)
+njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc, void *promise_cap,
+    void *async_ctx)
 {
     u_char                       *catch;
     double                       num, exponent;
     int32_t                      i32;
     uint32_t                     u32;
@@ -824,11 +826,11 @@ next:
 
                 break;
 
             case NJS_VMCODE_AWAIT:
                 await = (njs_vmcode_await_t *) pc;
-                return njs_vmcode_await(vm, await);
+                return njs_vmcode_await(vm, await, promise_cap, async_ctx);
 
             case NJS_VMCODE_TRY_START:
                 ret = njs_vmcode_try_start(vm, value1, value2, pc);
                 if (njs_slow_path(ret == NJS_ERROR)) {
                     goto error;
@@ -1810,19 +1812,19 @@ njs_vmcode_return(njs_vm_t *vm, njs_value_t *invld, njs_value_t *retval)
     return NJS_OK;
 }
 
 
 static njs_jump_off_t
-njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await)
+njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await,
+    njs_promise_capability_t *pcap, njs_async_ctx_t *ctx)
 {
     size_t              size;
     njs_int_t           ret;
     njs_frame_t         *frame;
     njs_value_t         ctor, val, on_fulfilled, on_rejected, *value;
     njs_promise_t       *promise;
     njs_function_t      *fulfilled, *rejected;
-    njs_async_ctx_t     *ctx;
     njs_native_frame_t  *active;
 
     active = &vm->active_frame->native;
 
     value = njs_scope_valid_value(vm, await->retval);
@@ -1835,12 +1837,10 @@ njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await)
     promise = njs_promise_resolve(vm, &ctor, value);
     if (njs_slow_path(promise == NULL)) {
         return NJS_ERROR;
     }
 
-    ctx = active->function->await;
-
     if (ctx == NULL) {
         ctx = njs_mp_alloc(vm->mem_pool, sizeof(njs_async_ctx_t));
         if (njs_slow_path(ctx == NULL)) {
             njs_memory_error(vm);
             return NJS_ERROR;
@@ -1852,13 +1852,11 @@ njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await)
         if (njs_slow_path(fulfilled == NULL)) {
             return NJS_ERROR;
         }
 
         ctx->await = fulfilled->context;
-        ctx->capability = active->function->context;
-
-        active->function->context = NULL;
+        ctx->capability = pcap;
 
         ret = njs_function_frame_save(vm, ctx->await, NULL);
         if (njs_slow_path(ret != NJS_OK)) {
             return NJS_ERROR;
         }
diff --git a/src/njs_vmcode.h b/src/njs_vmcode.h
index c15a4bd9..6b8f6558 100644
--- a/src/njs_vmcode.h
+++ b/src/njs_vmcode.h
@@ -435,11 +435,12 @@ typedef struct {
     njs_vmcode_t               code;
     njs_index_t                retval;
 } njs_vmcode_await_t;
 
 
-njs_int_t njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc);
+njs_int_t njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc,
+    void *promise_cap, void *async_ctx);
 
 njs_object_t *njs_function_new_object(njs_vm_t *vm, njs_value_t *constructor);
 
 
 #endif /* _NJS_VMCODE_H_INCLUDED_ */
diff --git a/test/js/async_recursive_last.t.js b/test/js/async_recursive_last.t.js
new file mode 100644
index 00000000..84f1b579
--- /dev/null
+++ b/test/js/async_recursive_last.t.js
@@ -0,0 +1,26 @@
+/*---
+includes: [compareArray.js]
+flags: [async]
+---*/
+
+let stages = [];
+
+async function f(v) {
+    if (v == 3) {
+        return;
+    }
+
+    stages.push(`f>${v}`);
+
+    f(v + 1);
+
+    stages.push(`f<${v}`);
+
+    await "X";
+}
+
+f(0)
+.then(v => {
+    assert.compareArray(stages, ['f>0', 'f>1', 'f>2', 'f<2', 'f<1', 'f<0']);
+})
+.then($DONE, $DONE);
diff --git a/test/js/async_recursive_mid.t.js b/test/js/async_recursive_mid.t.js
new file mode 100644
index 00000000..4d3a9fd1
--- /dev/null
+++ b/test/js/async_recursive_mid.t.js
@@ -0,0 +1,26 @@
+/*---
+includes: [compareArray.js]
+flags: [async]
+---*/
+
+let stages = [];
+
+async function f(v) {
+    if (v == 3) {
+        return;
+    }
+
+    stages.push(`f>${v}`);
+
+    await "X";
+
+    f(v + 1);
+
+    stages.push(`f<${v}`);
+}
+
+f(0)
+.then(v => {
+    assert.compareArray(stages, ['f>0','f>1','f<0','f>2','f<1']);
+})
+.then($DONE, $DONE);

Vulnerable code:     frame->function->context = capability;

    ret = njs_function_lambda_call(vm);
    njs_function_t      *function;
    function = async->function;

    function->context = ctx->capability;
    function->await = ctx;

    ret = njs_vmcode_interpreter(vm, ctx->pc);

    function->context = NULL;
    function->await = NULL;
njs_function_lambda_call(njs_vm_t *vm)
    ret = njs_vmcode_interpreter(vm, lambda->start);
        return njs_function_lambda_call(vm);
njs_int_t njs_function_lambda_call(njs_vm_t *vm);
    void                              *await;
    ret = njs_vmcode_interpreter(vm, vm->start);
static njs_jump_off_t njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await);
njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc)
                return njs_vmcode_await(vm, await);
njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await)
    njs_async_ctx_t     *ctx;
    ctx = active->function->await;

        ctx->capability = active->function->context;

        active->function->context = NULL;
njs_int_t njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc);
Fixed code:     ret = njs_function_lambda_call(vm, capability, NULL);
    ret = njs_vmcode_interpreter(vm, ctx->pc, ctx->capability, ctx);
njs_function_lambda_call(njs_vm_t *vm, void *promise_cap, void *async_ctx)
    njs_assert(function->context == NULL);

    ret = njs_vmcode_interpreter(vm, lambda->start, promise_cap, async_ctx);
        return njs_function_lambda_call(vm, NULL, NULL);
njs_int_t njs_function_lambda_call(njs_vm_t *vm, void *promise_cap,
    void *async_ctx);
    ret = njs_vmcode_interpreter(vm, vm->start, NULL, NULL);
static njs_jump_off_t njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await,
    njs_promise_capability_t *pcap, njs_async_ctx_t *actx);
njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc, void *promise_cap,
    void *async_ctx)
                return njs_vmcode_await(vm, await, promise_cap, async_ctx);
njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await,
    njs_promise_capability_t *pcap, njs_async_ctx_t *ctx)
        ctx->capability = pcap;
njs_int_t njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc,
    void *promise_cap, void *async_ctx);
/*---
includes: [compareArray.js]
flags: [async]
---*/

let stages = [];

async function f(v) {
    if (v == 3) {
        return;
    }

    stages.push(`f>${v}`);

    f(v + 1);

    stages.push(`f<${v}`);

    await "X";
}

f(0)
.then(v => {
    assert.compareArray(stages, ['f>0', 'f>1', 'f>2', 'f<2', 'f<1', 'f<0']);
})
.then($DONE, $DONE);
/*---
includes: [compareArray.js]
flags: [async]
---*/

let stages = [];

async function f(v) {
    if (v == 3) {
        return;
    }

    stages.push(`f>${v}`);

    await "X";

    f(v + 1);

    stages.push(`f<${v}`);
}

f(0)
.then(v => {
    assert.compareArray(stages, ['f>0','f>1','f<0','f>2','f<1']);
})
.then($DONE, $DONE);