Skip to content

Commit dd95fb7

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 bb35b6e commit dd95fb7

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,
@@ -668,11 +669,17 @@ async function writeFile(path, data, options) {
668669
data = Buffer.from(data, options.encoding || 'utf8');
669670
}
670671

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

676+
if (options.signal?.aborted) {
677+
throw lazyDOMException('The operation was aborted', 'AbortError');
678+
}
679+
674680
const fd = await open(path, flag, options.mode);
675681
if (options.signal?.aborted) {
682+
await fd.close();
676683
throw lazyDOMException('The operation was aborted', 'AbortError');
677684
}
678685
return PromisePrototypeFinally(writeFileHandle(fd, data), fd.close);
@@ -692,6 +699,10 @@ async function readFile(path, options) {
692699
if (path instanceof FileHandle)
693700
return readFileHandle(path, options);
694701

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

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)