Skip to content
Closed
Next Next commit
fs: guard against undefined behavior
Calling close on a file description which is currently in use is
undefined behavior due to implementation details in libuv. Add
a guard against this when using FileHandle.
  • Loading branch information
ronag committed Aug 12, 2020
commit ceb22f5197223f68037fe99155f77f64aa44cfd1
64 changes: 48 additions & 16 deletions lib/internal/fs/promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
MathMin,
NumberIsSafeInteger,
Symbol,
Promise,
} = primordials;

const {
Expand Down Expand Up @@ -64,18 +65,43 @@ const { promisify } = require('internal/util');

const kHandle = Symbol('kHandle');
const kFd = Symbol('kFd');
const kRefs = Symbol('kRefs');
const kClosePromise = Symbol('kClosePromise');
const kCloseResolve = Symbol('kCloseResolve');
const kCloseReject = Symbol('kCloseReject');

const { kUsePromises } = binding;
const {
JSTransferable, kDeserialize, kTransfer, kTransferList
} = require('internal/worker/js_transferable');

const getDirectoryEntriesPromise = promisify(getDirents);

async function fsCall(fn, fileHandle, ...args) {
try {
fileHandle[kRefs]++;
return await fn(fileHandle, ...args);
} finally {
fileHandle[kRefs]--;

if (fileHandle[kRefs] === 0) {
this[kFd] = -1;
this[kHandle].close().then(this[kCloseResolve], this[kCloseReject]);
}
}
}

class FileHandle extends JSTransferable {
constructor(filehandle) {
super();
this[kHandle] = filehandle;
this[kFd] = filehandle ? filehandle.fd : -1;

this[kRefs] = 1;
this[kClosePromise] = new Promise((resolve, reject) => {
this[kCloseResolve] = resolve;
this[kCloseReject] = reject;
});
}

getAsyncId() {
Expand All @@ -87,64 +113,70 @@ class FileHandle extends JSTransferable {
}

appendFile(data, options) {
return writeFile(this, data, options);
return fsCall(writeFile, this, data, options);
}

chmod(mode) {
return fchmod(this, mode);
return fsCall(fchmod, this, mode);
}

chown(uid, gid) {
return fchown(this, uid, gid);
return fsCall(fchown, this, uid, gid);
}

datasync() {
return fdatasync(this);
return fsCall(fdatasync, this);
}

sync() {
return fsync(this);
return fsCall(fsync, this);
}

read(buffer, offset, length, position) {
return read(this, buffer, offset, length, position);
return fsCall(read, this, buffer, offset, length, position);
}

readv(buffers, position) {
return readv(this, buffers, position);
return fsCall(readv, this, buffers, position);
}

readFile(options) {
return readFile(this, options);
return fsCall(readFile, this, options);
}

stat(options) {
return fstat(this, options);
return fsCall(fstat, this, options);
}

truncate(len = 0) {
return ftruncate(this, len);
return fsCall(ftruncate, this, len);
}

utimes(atime, mtime) {
return futimes(this, atime, mtime);
return fsCall(futimes, this, atime, mtime);
}

write(buffer, offset, length, position) {
return write(this, buffer, offset, length, position);
return fsCall(write, this, buffer, offset, length, position);
}

writev(buffers, position) {
return writev(this, buffers, position);
return fsCall(writev, this, buffers, position);
}

writeFile(data, options) {
return writeFile(this, data, options);
return fsCall(writeFile, this, data, options);
}

close = () => {
this[kFd] = -1;
return this[kHandle].close();
this[kRefs]--;

if (this[kRefs] === 0) {
this[kFd] = -1;
this[kHandle].close().then(this[kCloseResolve], this[kCloseReject]);
}

return this[kClosePromise];
}

[kTransfer]() {
Expand Down