details:   
https://github.com/nginx/njs/commit/ef7ef9fe94fdcb029f5a61bed64067e303d4bb49
branches:  master
commit:    ef7ef9fe94fdcb029f5a61bed64067e303d4bb49
user:      Dmitry Volyntsev <xei...@nginx.com>
date:      Wed, 11 Jun 2025 16:17:42 -0700
description:
Fixed frame saving for awaited function with closures.

The issue was introduced in 6335367 (0.7.0).

This fixes #530 issue on Github.

---
 src/njs_function.c               | 88 ++++++++++++++++++++++------------------
 src/njs_function.h               | 26 +++---------
 test/js/async_closure.t.js       | 24 +++++++++++
 test/js/async_closure_arg.t.js   | 24 +++++++++++
 test/js/async_closure_share.t.js | 28 +++++++++++++
 5 files changed, 130 insertions(+), 60 deletions(-)

diff --git a/src/njs_function.c b/src/njs_function.c
index 06948177..90a54204 100644
--- a/src/njs_function.c
+++ b/src/njs_function.c
@@ -394,6 +394,19 @@ njs_function_lambda_frame(njs_vm_t *vm, njs_function_t 
*function,
 
     lambda = function->u.lambda;
 
+    /*
+     * Lambda frame has the following layout:
+     *  njs_frame_t | p0 , p2, ..., pn | v0, v1, ..., vn
+     *  where:
+     *  p0, p1, ..., pn - pointers to arguments and locals,
+     *  v0, v1, ..., vn - values of arguments and locals.
+     *  n - number of arguments + locals.
+     *
+     *  Normally, the pointers point to the values directly after them,
+     *  but if a value was captured as a closure by an inner function,
+     *  pn points to a value allocated from the heap.
+     */
+
     args_count = njs_max(nargs, lambda->nargs);
     value_count = args_count + lambda->nlocal;
 
@@ -675,11 +688,11 @@ njs_function_frame_free(njs_vm_t *vm, njs_native_frame_t 
*native)
 njs_int_t
 njs_function_frame_save(njs_vm_t *vm, njs_frame_t *frame, u_char *pc)
 {
-    size_t              args_count, value_count, n;
-    njs_value_t         *start, *end, *p, **new, *value, **local;
-    njs_function_t      *function;
+    size_t                 args_count, value_count, n;
+    njs_value_t            **map, *value, **current_map;
+    njs_function_t         *function;
+    njs_native_frame_t     *active, *native;
     njs_function_lambda_t  *lambda;
-    njs_native_frame_t  *active, *native;
 
     *frame = *vm->active_frame;
 
@@ -697,34 +710,37 @@ njs_function_frame_save(njs_vm_t *vm, njs_frame_t *frame, 
u_char *pc)
     args_count = njs_max(native->nargs, lambda->nargs);
     value_count = args_count + lambda->nlocal;
 
-    new = (njs_value_t **) ((u_char *) native + NJS_FRAME_SIZE);
-    value = (njs_value_t *) (new + value_count);
-
-    native->arguments = value;
-    native->local = new + njs_function_frame_args_count(active);
-    native->pc = pc;
-
-    start = njs_function_frame_values(active, &end);
-    p = native->arguments;
+    /*
+     * We need to save the current frame state because it will be freed
+     * when the function returns.
+     *
+     *  To detect whether a value is captured as a closure,
+     *  we check whether the pointer is within the frame. In this case
+     *  the pointer is copied as is because the value it points to
+     *  is already allocated in the heap and will not be freed.
+     *  See njs_function_capture_closure() and njs_function_lambda_frame()
+     *  for details.
+     */
 
-    while (start < end) {
-        njs_value_assign(p, start++);
-        *new++ = p++;
-    }
+    map = (njs_value_t **) ((u_char *) native + NJS_FRAME_SIZE);
+    value = (njs_value_t *) (map + value_count);
 
-    /* Move all arguments. */
+    current_map = (njs_value_t **) ((u_char *) active + NJS_FRAME_SIZE);
 
-    p = native->arguments;
-    local = native->local + 1 /* this */;
+    for (n = 0; n < value_count; n++) {
+        if (njs_is_value_allocated_on_frame(active, current_map[n])) {
+            map[n] = &value[n];
+            njs_value_assign(&value[n], current_map[n]);
 
-    for (n = 0; n < function->args_count; n++) {
-        if (!njs_is_valid(p)) {
-            njs_set_undefined(p);
+        } else {
+            map[n] = current_map[n];
         }
-
-        *local++ = p++;
     }
 
+    native->arguments = value;
+    native->local = map + args_count;
+    native->pc = pc;
+
     return NJS_OK;
 }
 
@@ -746,7 +762,6 @@ njs_int_t
 njs_function_capture_closure(njs_vm_t *vm, njs_function_t *function,
     njs_function_lambda_t *lambda)
 {
-    void                *start, *end;
     uint32_t            n;
     njs_value_t         *value, **closure;
     njs_native_frame_t  *frame;
@@ -761,9 +776,6 @@ njs_function_capture_closure(njs_vm_t *vm, njs_function_t 
*function,
         frame = frame->previous;
     }
 
-    start = frame;
-    end = frame->free;
-
     closure = njs_function_closures(function);
     n = lambda->nclosures;
 
@@ -772,7 +784,7 @@ njs_function_capture_closure(njs_vm_t *vm, njs_function_t 
*function,
 
         value = njs_scope_value(vm, lambda->closures[n]);
 
-        if (start <= (void *) value && (void *) value < end) {
+        if (njs_is_value_allocated_on_frame(frame, value)) {
             value = njs_scope_value_clone(vm, lambda->closures[n], value);
             if (njs_slow_path(value == NULL)) {
                 return NJS_ERROR;
@@ -788,14 +800,14 @@ njs_function_capture_closure(njs_vm_t *vm, njs_function_t 
*function,
 
 
 njs_inline njs_value_t *
-njs_function_closure_value(njs_vm_t *vm, njs_value_t **scope, njs_index_t 
index,
-    void *start, void *end)
+njs_function_closure_value(njs_vm_t *vm, njs_native_frame_t *frame,
+    njs_value_t **scope, njs_index_t index)
 {
     njs_value_t  *value, *newval;
 
     value = scope[njs_scope_index_value(index)];
 
-    if (start <= (void *) value && end > (void *) value) {
+    if (njs_is_value_allocated_on_frame(frame, value)) {
         newval = njs_mp_alloc(vm->mem_pool, sizeof(njs_value_t));
         if (njs_slow_path(newval == NULL)) {
             njs_memory_error(vm);
@@ -815,7 +827,6 @@ njs_function_closure_value(njs_vm_t *vm, njs_value_t 
**scope, njs_index_t index,
 njs_int_t
 njs_function_capture_global_closures(njs_vm_t *vm, njs_function_t *function)
 {
-    void                   *start, *end;
     uint32_t               n;
     njs_value_t            *value, **refs, **global;
     njs_index_t            *indexes, index;
@@ -834,9 +845,6 @@ njs_function_capture_global_closures(njs_vm_t *vm, 
njs_function_t *function)
         native = native->previous;
     }
 
-    start = native;
-    end = native->free;
-
     indexes = lambda->closures;
     refs = njs_function_closures(function);
 
@@ -851,12 +859,12 @@ njs_function_capture_global_closures(njs_vm_t *vm, 
njs_function_t *function)
 
         switch (njs_scope_index_type(index)) {
         case NJS_LEVEL_LOCAL:
-            value = njs_function_closure_value(vm, native->local, index,
-                                               start, end);
+            value = njs_function_closure_value(vm, native, native->local,
+                                               index);
             break;
 
         case NJS_LEVEL_GLOBAL:
-            value = njs_function_closure_value(vm, global, index, start, end);
+            value = njs_function_closure_value(vm, native, global, index);
             break;
 
         default:
diff --git a/src/njs_function.h b/src/njs_function.h
index 5aa98dd3..1b5de544 100644
--- a/src/njs_function.h
+++ b/src/njs_function.h
@@ -202,29 +202,15 @@ njs_function_frame_size(njs_native_frame_t *frame)
 }
 
 
-njs_inline size_t
-njs_function_frame_args_count(njs_native_frame_t *frame)
-{
-    uintptr_t  start;
-
-    start = (uintptr_t) ((u_char *) frame + NJS_FRAME_SIZE);
-
-    return ((uintptr_t) frame->local - start) / sizeof(njs_value_t *);
-}
-
-
-njs_inline njs_value_t *
-njs_function_frame_values(njs_native_frame_t *frame, njs_value_t **end)
+njs_inline njs_bool_t
+njs_is_value_allocated_on_frame(njs_native_frame_t *frame, njs_value_t *value)
 {
-    size_t     count;
-    uintptr_t  start;
-
-    start = (uintptr_t) ((u_char *) frame + NJS_FRAME_SIZE);
-    count = ((uintptr_t) frame->arguments - start) / sizeof(njs_value_t *);
+    void  *start, *end;
 
-    *end = frame->arguments + count;
+    start = frame;
+    end = frame->free;
 
-    return frame->arguments;
+    return start <= (void *) value && (void *) value < end;
 }
 
 
diff --git a/test/js/async_closure.t.js b/test/js/async_closure.t.js
new file mode 100644
index 00000000..9a387ff2
--- /dev/null
+++ b/test/js/async_closure.t.js
@@ -0,0 +1,24 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+async function f() {
+    await 1;
+    var v = 2;
+
+    function g() {
+      return v + 1;
+    }
+
+    function s() {
+      g + 1;
+    }
+
+    return g();
+}
+
+f().then(v => {
+    assert.sameValue(v, 3)
+})
+.then($DONE, $DONE);
diff --git a/test/js/async_closure_arg.t.js b/test/js/async_closure_arg.t.js
new file mode 100644
index 00000000..d6aa8ab6
--- /dev/null
+++ b/test/js/async_closure_arg.t.js
@@ -0,0 +1,24 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+async function f(v) {
+    await 1;
+    v = 2;
+
+    function g() {
+      return v + 1;
+    }
+
+    function s() {
+      g + 1;
+    }
+
+    return g();
+}
+
+f(42).then(v => {
+    assert.sameValue(v, 3)
+})
+.then($DONE, $DONE);
diff --git a/test/js/async_closure_share.t.js b/test/js/async_closure_share.t.js
new file mode 100644
index 00000000..d78f92c5
--- /dev/null
+++ b/test/js/async_closure_share.t.js
@@ -0,0 +1,28 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+async function f() {
+    await 1;
+    var v = 'f';
+
+    function g() {
+        v += ':g';
+        return v;
+    }
+
+    function s() {
+        v += ':s';
+        return v;
+    }
+
+    return [g, s];
+}
+
+f().then(pair => {
+    pair[0]();
+    var v = pair[1]();
+    assert.sameValue(v, 'f:g:s');
+})
+.then($DONE, $DONE);
_______________________________________________
nginx-devel mailing list
nginx-devel@nginx.org
https://mailman.nginx.org/mailman/listinfo/nginx-devel

Reply via email to