Possible use-after-free of a JS Buffer captured by the async WriteArea worker
I found a possible borrowed-buffer-across-async use-after-free in S7Client::WriteArea.
When a completion callback is supplied, WriteArea takes the raw backing-store pointer of the
JS Buffer argument via node::Buffer::Data(info[5]) and hands it to a libuv-threadpool
IOWorker (caller WRITEAREA). The worker keeps only a bare void* pData — it never creates a
napi_reference/Nan::Persistent on the buffer and the JS wrapper (lib/node-snap7.js,
WriteArea/DBWrite/MBWrite/…) does not retain it either. Once the synchronous call returns
undefined, nothing keeps the Buffer alive; if JS drops its last reference the garbage
collector can free (or relocate) the backing store while the worker thread is still reading from
that pointer inside snap7Client->WriteArea(...), producing a use-after-free / read of freed
memory sent to the PLC.
File: src/node_snap7_client.cpp
Function: S7Client::WriteArea
} else {
Nan::Callback *callback = new Nan::Callback(info[6].As<v8::Function>());
Nan::AsyncQueueWorker(new IOWorker(callback, s7client, WRITEAREA
, node::Buffer::Data(info[5].As<v8::Object>()), Nan::To<int32_t>(info[0]).FromJust()
, Nan::To<int32_t>(info[1]).FromJust(), Nan::To<int32_t>(info[2]).FromJust(), Nan::To<int32_t>(info[3]).FromJust()
, Nan::To<int32_t>(info[4]).FromJust()));
info.GetReturnValue().SetUndefined();
}
The IOWorker constructor only stores the pointer (node_snap7_client.h):
IOWorker(Nan::Callback *callback, S7Client *s7client, DataIOFunction caller
, void *arg1, int arg2, int arg3, int arg4, int arg5, int arg6)
: Nan::AsyncWorker(callback), s7client(s7client), caller(caller)
, pData(arg1), int1(arg2), int2(arg3), int3(arg4), int4(arg5), int5(arg6) {}
and IOWorker::Execute() (which runs on the threadpool thread) dereferences it:
case WRITEAREA:
returnValue = s7client->snap7Client->WriteArea(int1, int2, int3, int4
, int5, pData);
Path:
writeArea(area, db, start, amount, wordlen, buf, cb) reaches the callback branch.
node::Buffer::Data(info[5]) returns a pointer into buf's V8 backing store.
- That raw pointer is stored in the worker's
pData and the worker is queued.
- There is no
SaveToPersistent/Nan::Persistent/napi_create_reference anywhere pinning
buf, and the sync call returns immediately.
- If JS no longer references
buf, a GC before/while Execute() runs frees or moves the
backing store; the worker thread then reads freed memory → UAF.
JS trigger (if applicable):
const snap7 = require('node-snap7');
const c = new snap7.S7Client();
// buf is not retained anywhere after this call returns
c.WriteArea(c.S7AreaDB, 1, 0, 100, c.S7WLByte, Buffer.alloc(100), (err) => {});
// allocate pressure to make GC reclaim the buffer while the worker thread runs
for (let i = 0; i < 1e6; i++) ({});
Suggested fix: pin the buffer for the worker's lifetime. Store a Nan::Persistent<v8::Object>
(e.g. via worker->SaveToPersistent("buffer", info[5])) inside the IOWorker for the
WRITEAREA/DOWNLOAD/WRITEMULTI callers, or copy the buffer bytes into a worker-owned heap
allocation before queuing, and release the reference / free the copy in HandleOKCallback.
Additional defect: unchecked buffer length vs Amount (out-of-bounds read)
Independently of the UAF, WriteArea never checks the caller Buffer's byte length against the
number of bytes snap7 will read from it. The bytes read are Amount * bytesPerWordLen(WordLen)
(info[3] = Amount, info[4] = WordLen), but only node::Buffer::HasInstance(info[5]) is
validated — node::Buffer::Length(info[5]) is never consulted. snap7's WriteArea then reads
Amount * bytesPerWordLen bytes from the borrowed pointer regardless of the buffer's real size:
if (!info[0]->IsInt32() || !info[1]->IsInt32() ||
!info[2]->IsInt32() || !info[3]->IsInt32() ||
!info[4]->IsInt32() || !node::Buffer::HasInstance(info[5])) // length of info[5] never checked
return Nan::ThrowTypeError("Wrong arguments");
...
// snap7Client->WriteArea(Area, DBNumber, Start, Amount=info[3], WordLen=info[4],
// pUsrData = Buffer::Data(info[5])) reads Amount*bytesPerWordLen bytes
A caller passing a small Buffer with a large Amount makes snap7 read past the end of the
buffer's backing store and transmit those out-of-bounds bytes to the PLC — a deterministic OOB
read / heap-memory disclosure that occurs on both the sync and async paths (no GC race required).
Additional fix (OOB read): reject the call when
node::Buffer::Length(info[5]) < Amount * GetByteCountFromWordLen(WordLen) before using the
buffer.
Possible use-after-free of a JS Buffer captured by the async
WriteAreaworkerI found a possible borrowed-buffer-across-async use-after-free in
S7Client::WriteArea.When a completion callback is supplied,
WriteAreatakes the raw backing-store pointer of theJS
Bufferargument vianode::Buffer::Data(info[5])and hands it to a libuv-threadpoolIOWorker(callerWRITEAREA). The worker keeps only a barevoid* pData— it never creates anapi_reference/Nan::Persistenton the buffer and the JS wrapper (lib/node-snap7.js,WriteArea/DBWrite/MBWrite/…) does not retain it either. Once the synchronous call returnsundefined, nothing keeps theBufferalive; if JS drops its last reference the garbagecollector can free (or relocate) the backing store while the worker thread is still reading from
that pointer inside
snap7Client->WriteArea(...), producing a use-after-free / read of freedmemory sent to the PLC.
File:
src/node_snap7_client.cppFunction:
S7Client::WriteAreaThe
IOWorkerconstructor only stores the pointer (node_snap7_client.h):and
IOWorker::Execute()(which runs on the threadpool thread) dereferences it:Path:
writeArea(area, db, start, amount, wordlen, buf, cb)reaches the callback branch.node::Buffer::Data(info[5])returns a pointer intobuf's V8 backing store.pDataand the worker is queued.SaveToPersistent/Nan::Persistent/napi_create_referenceanywhere pinningbuf, and the sync call returns immediately.buf, a GC before/whileExecute()runs frees or moves thebacking store; the worker thread then reads freed memory → UAF.
JS trigger (if applicable):
Suggested fix: pin the buffer for the worker's lifetime. Store a
Nan::Persistent<v8::Object>(e.g. via
worker->SaveToPersistent("buffer", info[5])) inside theIOWorkerfor theWRITEAREA/DOWNLOAD/WRITEMULTI callers, or copy the buffer bytes into a worker-owned heap
allocation before queuing, and release the reference / free the copy in
HandleOKCallback.Additional defect: unchecked buffer length vs
Amount(out-of-bounds read)Independently of the UAF,
WriteAreanever checks the callerBuffer's byte length against thenumber of bytes snap7 will read from it. The bytes read are
Amount * bytesPerWordLen(WordLen)(
info[3]= Amount,info[4]= WordLen), but onlynode::Buffer::HasInstance(info[5])isvalidated —
node::Buffer::Length(info[5])is never consulted. snap7'sWriteAreathen readsAmount * bytesPerWordLenbytes from the borrowed pointer regardless of the buffer's real size:A caller passing a small
Bufferwith a largeAmountmakes snap7 read past the end of thebuffer's backing store and transmit those out-of-bounds bytes to the PLC — a deterministic OOB
read / heap-memory disclosure that occurs on both the sync and async paths (no GC race required).
Additional fix (OOB read): reject the call when
node::Buffer::Length(info[5]) < Amount * GetByteCountFromWordLen(WordLen)before using thebuffer.