Skip to content

Commit 8be3c97

Browse files
joyeecheungtargos
authored andcommitted
fs: do not crash when using a closed fs event watcher
Before this commit, when the user calls methods on a closed or errored fs event watcher, they could hit a crash since the FSEventWrap in C++ land may have already been destroyed with the internal pointer set to nullptr. This commit makes sure that the user cannot hit crashes like that, instead the methods calling on a closed watcher will be noops. Also explicitly documents that the watchers should not be used in `close` and `error` event handlers. PR-URL: #20985 Fixes: #20738 Fixes: #20297 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Ruben Bridgewater <[email protected]> Reviewed-By: Ron Korving <[email protected]> Reviewed-By: Sakthipriyan Vairamani <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Tiancheng "Timothy" Gu <[email protected]> Backport-PR-URL: #21172
1 parent 4b278d1 commit 8be3c97

File tree

4 files changed

+74
-10
lines changed

4 files changed

+74
-10
lines changed

doc/api/fs.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,8 @@ fs.watch('./tmp', { encoding: 'buffer' }, (eventType, filename) => {
325325
added: v10.0.0
326326
-->
327327

328-
Emitted when the watcher stops watching for changes.
328+
Emitted when the watcher stops watching for changes. The closed
329+
`fs.FSWatcher` object is no longer usable in the event handler.
329330

330331
### Event: 'error'
331332
<!-- YAML
@@ -334,7 +335,8 @@ added: v0.5.8
334335

335336
* `error` {Error}
336337

337-
Emitted when an error occurs while watching the file.
338+
Emitted when an error occurs while watching the file. The errored
339+
`fs.FSWatcher` object is no longer usable in the event handler.
338340

339341
### watcher.close()
340342
<!-- YAML

lib/internal/fs/watchers.js

+17-4
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ function FSWatcher() {
100100
// after the handle is closed, and to fire both UV_RENAME and UV_CHANGE
101101
// if they are set by libuv at the same time.
102102
if (status < 0) {
103-
this._handle.close();
103+
if (this._handle !== null) {
104+
// We don't use this.close() here to avoid firing the close event.
105+
this._handle.close();
106+
this._handle = null; // make the handle garbage collectable
107+
}
104108
const error = errors.uvException({
105109
errno: status,
106110
syscall: 'watch',
@@ -120,13 +124,17 @@ util.inherits(FSWatcher, EventEmitter);
120124
// 1. Throw an Error if it's the first time .start() is called
121125
// 2. Return silently if .start() has already been called
122126
// on a valid filename and the wrap has been initialized
127+
// 3. Return silently if the watcher has already been closed
123128
// This method is a noop if the watcher has already been started.
124129
FSWatcher.prototype.start = function(filename,
125130
persistent,
126131
recursive,
127132
encoding) {
133+
if (this._handle === null) { // closed
134+
return;
135+
}
128136
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
129-
if (this._handle.initialized) {
137+
if (this._handle.initialized) { // already started
130138
return;
131139
}
132140

@@ -148,13 +156,18 @@ FSWatcher.prototype.start = function(filename,
148156
}
149157
};
150158

151-
// This method is a noop if the watcher has not been started.
159+
// This method is a noop if the watcher has not been started or
160+
// has already been closed.
152161
FSWatcher.prototype.close = function() {
162+
if (this._handle === null) { // closed
163+
return;
164+
}
153165
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
154-
if (!this._handle.initialized) {
166+
if (!this._handle.initialized) { // not started
155167
return;
156168
}
157169
this._handle.close();
170+
this._handle = null; // make the handle garbage collectable
158171
process.nextTick(emitCloseNT, this);
159172
};
160173

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict';
2+
3+
// This tests that closing a watcher when the underlying handle is
4+
// already destroyed will result in a noop instead of a crash.
5+
6+
const common = require('../common');
7+
const tmpdir = require('../common/tmpdir');
8+
const fs = require('fs');
9+
const path = require('path');
10+
11+
tmpdir.refresh();
12+
const root = path.join(tmpdir.path, 'watched-directory');
13+
fs.mkdirSync(root);
14+
15+
const watcher = fs.watch(root, { persistent: false, recursive: false });
16+
17+
// The following listeners may or may not be invoked.
18+
19+
watcher.addListener('error', () => {
20+
setTimeout(
21+
() => { watcher.close(); }, // Should not crash if it's invoked
22+
common.platformTimeout(10)
23+
);
24+
});
25+
26+
watcher.addListener('change', () => {
27+
setTimeout(
28+
() => { watcher.close(); },
29+
common.platformTimeout(10)
30+
);
31+
});
32+
33+
fs.rmdirSync(root);
34+
// Wait for the listener to hit
35+
setTimeout(
36+
common.mustCall(() => {}),
37+
common.platformTimeout(100)
38+
);

test/parallel/test-fs-watch.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,20 @@ for (const testCase of cases) {
4646
fs.writeFileSync(testCase.filePath, content1);
4747

4848
let interval;
49-
const watcher = fs.watch(testCase[testCase.field]);
49+
const pathToWatch = testCase[testCase.field];
50+
const watcher = fs.watch(pathToWatch);
5051
watcher.on('error', (err) => {
5152
if (interval) {
5253
clearInterval(interval);
5354
interval = null;
5455
}
5556
assert.fail(err);
5657
});
57-
watcher.on('close', common.mustCall());
58+
watcher.on('close', common.mustCall(() => {
59+
watcher.close(); // Closing a closed watcher should be a noop
60+
// Starting a closed watcher should be a noop
61+
watcher.start();
62+
}));
5863
watcher.on('change', common.mustCall(function(eventType, argFilename) {
5964
if (interval) {
6065
clearInterval(interval);
@@ -66,10 +71,16 @@ for (const testCase of cases) {
6671
assert.strictEqual(eventType, 'change');
6772
assert.strictEqual(argFilename, testCase.fileName);
6873

69-
watcher.start(); // Starting a started watcher should be a noop
70-
// End of test case
74+
// Starting a started watcher should be a noop
75+
watcher.start();
76+
watcher.start(pathToWatch);
77+
7178
watcher.close();
79+
80+
// We document that watchers cannot be used anymore when it's closed,
81+
// here we turn the methods into noops instead of throwing
7282
watcher.close(); // Closing a closed watcher should be a noop
83+
watcher.start(); // Starting a closed watcher should be a noop
7384
}));
7485

7586
// Long content so it's actually flushed. toUpperCase so there's real change.

0 commit comments

Comments
 (0)