Skip to content

Possible use-after-free of a JS Buffer captured by the async Download worker #110

Description

@OvOhao

Possible use-after-free of a JS Buffer captured by the async Download worker

I found a possible borrowed-buffer-across-async use-after-free in S7Client::Download.
When a completion callback is supplied, Download passes the raw backing-store pointer of the
JS Buffer block argument via node::Buffer::Data(info[1]) (together with its length) to a
libuv-threadpool IOWorker (caller DOWNLOAD). The worker keeps only a bare void* pData; no
napi_reference/Nan::Persistent is created on the buffer, and the JS wrapper does not retain
it. Once the synchronous call returns undefined, nothing keeps the Buffer alive, so a GC can
free or relocate its backing store while the worker thread is still reading from that pointer
inside snap7Client->Download(...) → use-after-free.

File: src/node_snap7_client.cpp

Function: S7Client::Download

} else {
    Nan::Callback *callback = new Nan::Callback(info[2].As<v8::Function>());
    Nan::AsyncQueueWorker(new IOWorker(callback, s7client, DOWNLOAD
      , node::Buffer::Data(info[1].As<v8::Object>()), Nan::To<int32_t>(info[0]).FromJust()
      , static_cast<int>(node::Buffer::Length(info[1].As<v8::Object>()))));
    info.GetReturnValue().SetUndefined();
}

The worker dereferences the borrowed pointer on the threadpool thread (IOWorker::Execute()):

case DOWNLOAD:
    returnValue = s7client->snap7Client->Download(int1, pData, int2);

Path:

  1. node::Buffer::Data(info[1]) returns a pointer into the block Buffer's V8 backing store.
  2. That raw pointer is stored in the worker's pData and the worker is queued; the buffer is
    never pinned with a persistent reference.
  3. The sync call returns immediately.
  4. If JS drops the buffer, a GC before/while Execute() runs frees or moves the backing store;
    the worker thread then reads freed memory when uploading the block to the PLC → UAF.

JS trigger (if applicable):

const snap7 = require('node-snap7');
const c = new snap7.S7Client();
c.Download(-1, Buffer.alloc(1024), (err) => {});   // buffer not retained
for (let i = 0; i < 1e6; i++) ({});                // provoke GC while the worker runs

Suggested fix: pin the block buffer for the worker's lifetime (store an Nan::Persistent
reference — e.g. SaveToPersistent on the IOWorker), or copy its bytes into worker-owned
storage before queuing, releasing/freeing in HandleOKCallback.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions