Search code examples
node.jsv8

How to prevent objects from being automatically recycled in v8


I need to trigger the user's callback function at a specific time, but when the callback function is triggered, the object has been recycled and cannot be used. Is there any way to save the object without being recycled? After the callback is triggered, I want to manually reclaim the memory

LPVOID createCallbackFunc(void *lpMethod, Local<Function> *lpCB, Local<Context> *lpContext)
{
  /*
t_cb - 55                    - push rbp
014F0001- 48 8B EC              - mov rbp,rsp
014F0004- 48 81 EC 90010000     - sub rsp,00000190

014F000B- 48 89 4C 24 08        - mov [rsp+08],rcx
014F0010- 48 89 54 24 10        - mov [rsp+10],rdx
014F0015- 4C 89 44 24 18        - mov [rsp+18],r8
014F001A- 4C 89 4C 24 20        - mov [rsp+20],r9

014F001F- 48 B9 0000000000000000 - mov rcx,0000000000000000
014F0029- 48 BA 0000000000000000 - mov rdx,0000000000000000
014F0033- 4C 8D 44 24 08        - lea r8,[rsp+08]
014F0038- 4C 8D 4D 10           - lea r9,[rbp+10]

014F003C- 48 B8 F0ACAB14FE7F0000 - mov rax,user32.MessageBoxA
014F0046- FF D0                 - call rax

014F0048- 48 81 C4 90010000     - add rsp,00000190
014F004F- 48 8B E5              - mov rsp,rbp
014F0052- 5D                    - pop rbp
014F0053- C3                    - ret

*/

  string code_str = "55 48 8B EC 48 81 EC 90 01 00 00 48 89 4C 24 08 48 89 54 24 10 4C 89 44 24 18 4C 89 4C 24 20 48 B9 00 00 00 00 00 00 00 00 48 BA 00 00 00 00 00 00 00 00 4C 8D 44 24 08 4C 8D 4D 10 48 B8 F0 AC AB 14 FE 7F 00 00 FF D0 48 81 C4 90 01 00 00 48 8B E5 5D C3";

  vector<BYTE> code_bytes = byteStr2Bytes(code_str);

  memcpy_s(code_bytes.data() + 0x21, sizeof(uintptr_t), &lpCB, sizeof(uintptr_t));
  memcpy_s(code_bytes.data() + 0x2B, sizeof(uintptr_t), &lpContext, sizeof(uintptr_t));
  memcpy_s(code_bytes.data() + 0x3E, sizeof(uintptr_t), &lpMethod, sizeof(uintptr_t));

  auto newmem = VirtualAlloc(0, code_bytes.size() + sizeof(uintptr_t), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  memcpy_s(newmem, code_bytes.size(), code_bytes.data(), code_bytes.size());
  return newmem;
}

BOOL cb_test(Local<Function> &cb, Local<Context> &context, uintptr_t *a, uintptr_t *b)
{
  // Will not print, and will give an error
  if (cb->IsFunction())
  {
    printf("xxx\n");
  }
  return FALSE;
};

void callback(const FunctionCallbackInfo<Value> &args)
{
  Isolate *isolate = args.GetIsolate();
  auto context = isolate->GetCurrentContext();

  auto cb = args[0].As<Function>();
  LPVOID addr = createCallbackFunc(&cb_test, &cb, &context);

  args.GetReturnValue().Set(Number::New(isolate, (uintptr_t)addr));
}

My current environment is windows 10 and nodejs 14.15.4


Solution

  • Your example is incomplete (e.g. what does createCallbackFunc do?) so it's hard to be sure what's going on, but here are a few general points to consider:

    • Objects will not get garbage-collected as long as they are reachable. That's the point of a garbage collector.
    • You cannot free/reclaim objects manually that are managed by the garbage collector.
    • I suspect that in the case at hand, the issue is handles. A Local<...> is, as the name implies, meant to be used in a well-defined local scope. Its lifetime is tied to that of the surrounding HandleScope. If createCallbackFunc tries to stow it away somewhere, then it's expected that the Local will become invalid soon (which doesn't say anything about the object it originally pointed to, and has nothing to do with the garbage collector). You can learn more about handles and their lifetimes at https://v8.dev/docs/embed.
    • If what you want is a weak reference with a callback that'll get called when the object is garbage-collected, look into "weak persistents". Note that there are no guarantees about when exactly (or whether at all) such callbacks will be called (it's just "best effort", but e.g. when shutting down may well be skipped), so using them for critical resource management is a bad idea.

    EDIT after updated question: The source of createCallbackFunc reveals two issues:

    1. lpCB is a pointer to a stack-allocated object, with memcpy_s(..., &lpCB, ..) you're creating a copy of the address of this pointer. That can't work; firstly because lpCB itself is very short-lived, and secondly because the stack-allocated cb (in function callback) that it refers to is similarly short-lived. So any pointer created this way will point to random garbage as soon as the callback function returns. (This part of the problem is standard C++ and has nothing to do with V8 or Node or GC or Windows or any other specifics.)

    2. As I guessed in my earlier answer, you're effectively trying to create a long-lived copy of a Local. Use a Persistent to accomplish that. Please read the documentation.

    Summary: Don't use memcpy on v8::Local.