CVE-2022-25139 (C, published 2022-02-14T22:15Z)
Description: value: njs through 0.7.0, used in NGINX, was discovered to contain a heap use-after-free in njs_await_fulfilled.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-416 \u2014 Use After Free
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.
Repository: https://github.com/nginx/njs
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 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.


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);