Skip to content

Commit 6e0162c

Browse files
Nitzan UzielyLinkgoron
Nitzan Uziely
authored andcommitted
fs: fix pre-aborted writeFile AbortSignal file leak
Fix an issue in writeFile where a file is opened, and not closed if the abort signal is aborted after the file was opened but before writing began.
1 parent 88d9268 commit 6e0162c

File tree

2 files changed

+59
-3
lines changed

2 files changed

+59
-3
lines changed

lib/internal/fs/promises.js

+11
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const {
6666
const { opendir } = require('internal/fs/dir');
6767
const {
6868
parseFileMode,
69+
validateAbortSignal,
6970
validateBoolean,
7071
validateBuffer,
7172
validateInteger,
@@ -667,11 +668,17 @@ async function writeFile(path, data, options) {
667668
data = Buffer.from(data, options.encoding || 'utf8');
668669
}
669670

671+
validateAbortSignal(options.signal);
670672
if (path instanceof FileHandle)
671673
return writeFileHandle(path, data, options.signal);
672674

675+
if (options.signal?.aborted) {
676+
throw lazyDOMException('The operation was aborted', 'AbortError');
677+
}
678+
673679
const fd = await open(path, flag, options.mode);
674680
if (options.signal?.aborted) {
681+
await fd.close();
675682
throw lazyDOMException('The operation was aborted', 'AbortError');
676683
}
677684
return PromisePrototypeFinally(writeFileHandle(fd, data), fd.close);
@@ -691,6 +698,10 @@ async function readFile(path, options) {
691698
if (path instanceof FileHandle)
692699
return readFileHandle(path, options);
693700

701+
if (options.signal?.aborted) {
702+
throw lazyDOMException('The operation was aborted', 'AbortError');
703+
}
704+
694705
const fd = await open(path, flag, 0o666);
695706
return PromisePrototypeFinally(readFileHandle(fd, options), fd.close);
696707
}

test/parallel/test-fs-promises-writefile.js

+48-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
const common = require('../common');
44
const fs = require('fs');
5+
const os = require('os');
6+
const cp = require('child_process');
57
const fsPromises = fs.promises;
68
const path = require('path');
79
const tmpdir = require('../common/tmpdir');
@@ -11,7 +13,9 @@ const tmpDir = tmpdir.path;
1113
tmpdir.refresh();
1214

1315
const dest = path.resolve(tmpDir, 'tmp.txt');
14-
const otherDest = path.resolve(tmpDir, 'tmp-2.txt');
16+
const otherDest2 = path.resolve(tmpDir, 'tmp-2.txt');
17+
const otherDest3 = path.resolve(tmpDir, 'tmp-3.txt');
18+
const otherDest4 = path.resolve(tmpDir, 'tmp-4.txt');
1519
const buffer = Buffer.from('abc'.repeat(1000));
1620
const buffer2 = Buffer.from('xyz'.repeat(1000));
1721

@@ -25,9 +29,33 @@ async function doWriteWithCancel() {
2529
const controller = new AbortController();
2630
const { signal } = controller;
2731
process.nextTick(() => controller.abort());
28-
assert.rejects(fsPromises.writeFile(otherDest, buffer, { signal }), {
32+
assert.rejects(fsPromises.writeFile(otherDest2, buffer, { signal }), {
2933
name: 'AbortError'
30-
});
34+
}).then(common.mustCall(()=> {
35+
checkIfFileIsOpen(otherDest2);
36+
}));
37+
}
38+
39+
async function doWriteWithCancelPreSync() {
40+
const controller = new AbortController();
41+
const { signal } = controller;
42+
controller.abort();
43+
assert.rejects(fsPromises.writeFile(otherDest3, buffer, { signal }), {
44+
name: 'AbortError'
45+
}).then(common.mustCall(()=> {
46+
checkIfFileIsOpen(otherDest3);
47+
}));
48+
}
49+
50+
async function doWriteWithCancelPostSync() {
51+
const controller = new AbortController();
52+
const { signal } = controller;
53+
controller.abort();
54+
assert.rejects(fsPromises.writeFile(otherDest4, buffer, { signal }), {
55+
name: 'AbortError'
56+
}).then(common.mustCall(()=> {
57+
checkIfFileIsOpen(otherDest4);
58+
}));
3159
}
3260

3361
async function doAppend() {
@@ -55,4 +83,21 @@ doWrite()
5583
.then(doAppend)
5684
.then(doRead)
5785
.then(doReadWithEncoding)
86+
.then(doWriteWithCancelPreSync)
87+
.then(doWriteWithCancelPostSync)
5888
.then(common.mustCall());
89+
90+
function checkIfFileIsOpen(fileName) {
91+
const platform = os.platform();
92+
if (platform === 'linux' || platform === 'darwin') {
93+
// Tried other ways like rm/stat, but that didn't work.
94+
cp.exec(`lsof -p ${process.pid}`, common.mustCall((err, value) => {
95+
if (err) {
96+
assert.ifError(err);
97+
return;
98+
}
99+
assert.ok(!value.toString().includes(fileName),
100+
`${fileName} is still open, but should be closed`);
101+
}));
102+
}
103+
}

0 commit comments

Comments
 (0)