CVE-2022-27007: Use After Free (CWE-416)
value: nginx njs 0.7.2 is affected suffers from Use-after-free in njs_function_frame_alloc() when it try to invoke from a restored frame saved with njs_function_frame_save().
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-04-14T15:15Z
Repository: https://github.com/nginx/njs
Language: JavaScript
Files changed: src/njs_function.c, test/js/async_recursive_large.t.js, test/js/async_recursive_mid.t.js
Commit date: 2022-02-21 16:52:59 +0000
Commit message: Fixed frame allocation from an awaited frame.

njs_function_frame_save() is used to save the awaited frame when "await"
instruction is encountered. The saving was done as a memcpy() of
existing runtime frame.

njs_function_frame_alloc() is used to alloc a new function frame, this
function tries to use a spare preallocated memory from the previous
frame first.  Previously, this function might result in "use-after-free"
when invoked from a restored frame saved with njs_function_frame_save().
Because njs_function_frame_save() left pointers to the spare memory of
the original frame which may be already free when saved frame is
restored.

The fix is to erase fields for the spare memory from the saved frame.

This closes #469 issue on Github.

Diff: diff --git a/src/njs_function.c b/src/njs_function.c
index c9d4d973..78857e01 100644
--- a/src/njs_function.c
+++ b/src/njs_function.c
@@ -809,13 +809,17 @@ njs_function_frame_save(njs_vm_t *vm, njs_frame_t *frame, u_char *pc)
     njs_value_t         *start, *end, *p, **new, *value, **local;
     njs_function_t      *function;
     njs_native_frame_t  *active, *native;
 
     *frame = *vm->active_frame;
+
     frame->previous_active_frame = NULL;
 
     native = &frame->native;
+    native->size = 0;
+    native->free = NULL;
+    native->free_size = 0;
 
     active = &vm->active_frame->native;
     value_count = njs_function_frame_value_count(active);
 
     function = active->function;
diff --git a/test/js/async_recursive_large.t.js b/test/js/async_recursive_large.t.js
new file mode 100644
index 00000000..423e8d01
--- /dev/null
+++ b/test/js/async_recursive_large.t.js
@@ -0,0 +1,26 @@
+/*---
+includes: [compareArray.js]
+flags: [async]
+---*/
+
+let stages = [];
+
+async function f(v) {
+    if (v == 1000) {
+        return;
+    }
+
+    stages.push(`f>${v}`);
+
+    await "X";
+
+    await f(v + 1);
+
+    stages.push(`f<${v}`);
+}
+
+f(0)
+.then(v => {
+    assert.sameValue(stages.length, 2000);
+})
+.then($DONE, $DONE);
diff --git a/test/js/async_recursive_mid.t.js b/test/js/async_recursive_mid.t.js
index 4d3a9fd1..6b677962 100644
--- a/test/js/async_recursive_mid.t.js
+++ b/test/js/async_recursive_mid.t.js
@@ -4,11 +4,11 @@ flags: [async]
 ---*/
 
 let stages = [];
 
 async function f(v) {
-    if (v == 3) {
+    if (v == 1000) {
         return;
     }
 
     stages.push(`f>${v}`);
 

Vulnerable code:     if (v == 3) {
Fixed code: 
    native->size = 0;
    native->free = NULL;
    native->free_size = 0;
/*---
includes: [compareArray.js]
flags: [async]
---*/

let stages = [];

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

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

    await "X";

    await f(v + 1);

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

f(0)
.then(v => {
    assert.sameValue(stages.length, 2000);
})
.then($DONE, $DONE);
    if (v == 1000) {