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: https://github.com/nodejs/node/pull/20985
Fixes: https://github.com/nodejs/node/issues/20738
Fixes: https://github.com/nodejs/node/issues/20297
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Ruben Bridgewater <ruben@bridgewater.de>
Reviewed-By: Ron Korving <ron@ronkorving.nl>
Reviewed-By: Sakthipriyan Vairamani <thechargingvolcano@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
This commit is contained in:
Joyee Cheung 2018-05-27 06:07:29 +08:00
parent 997e97d9cd
commit cd8f06f64f
No known key found for this signature in database
GPG Key ID: 92B78A53C8303B8D
4 changed files with 74 additions and 10 deletions

View File

@ -325,7 +325,8 @@ fs.watch('./tmp', { encoding: 'buffer' }, (eventType, filename) => {
added: v10.0.0 added: v10.0.0
--> -->
Emitted when the watcher stops watching for changes. Emitted when the watcher stops watching for changes. The closed
`fs.FSWatcher` object is no longer usable in the event handler.
### Event: 'error' ### Event: 'error'
<!-- YAML <!-- YAML
@ -334,7 +335,8 @@ added: v0.5.8
* `error` {Error} * `error` {Error}
Emitted when an error occurs while watching the file. Emitted when an error occurs while watching the file. The errored
`fs.FSWatcher` object is no longer usable in the event handler.
### watcher.close() ### watcher.close()
<!-- YAML <!-- YAML

View File

@ -100,7 +100,11 @@ function FSWatcher() {
// after the handle is closed, and to fire both UV_RENAME and UV_CHANGE // after the handle is closed, and to fire both UV_RENAME and UV_CHANGE
// if they are set by libuv at the same time. // if they are set by libuv at the same time.
if (status < 0) { if (status < 0) {
this._handle.close(); if (this._handle !== null) {
// We don't use this.close() here to avoid firing the close event.
this._handle.close();
this._handle = null; // make the handle garbage collectable
}
const error = errors.uvException({ const error = errors.uvException({
errno: status, errno: status,
syscall: 'watch', syscall: 'watch',
@ -120,13 +124,17 @@ util.inherits(FSWatcher, EventEmitter);
// 1. Throw an Error if it's the first time .start() is called // 1. Throw an Error if it's the first time .start() is called
// 2. Return silently if .start() has already been called // 2. Return silently if .start() has already been called
// on a valid filename and the wrap has been initialized // on a valid filename and the wrap has been initialized
// 3. Return silently if the watcher has already been closed
// This method is a noop if the watcher has already been started. // This method is a noop if the watcher has already been started.
FSWatcher.prototype.start = function(filename, FSWatcher.prototype.start = function(filename,
persistent, persistent,
recursive, recursive,
encoding) { encoding) {
if (this._handle === null) { // closed
return;
}
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent'); assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (this._handle.initialized) { if (this._handle.initialized) { // already started
return; return;
} }
@ -148,13 +156,18 @@ FSWatcher.prototype.start = function(filename,
} }
}; };
// This method is a noop if the watcher has not been started. // This method is a noop if the watcher has not been started or
// has already been closed.
FSWatcher.prototype.close = function() { FSWatcher.prototype.close = function() {
if (this._handle === null) { // closed
return;
}
assert(this._handle instanceof FSEvent, 'handle must be a FSEvent'); assert(this._handle instanceof FSEvent, 'handle must be a FSEvent');
if (!this._handle.initialized) { if (!this._handle.initialized) { // not started
return; return;
} }
this._handle.close(); this._handle.close();
this._handle = null; // make the handle garbage collectable
process.nextTick(emitCloseNT, this); process.nextTick(emitCloseNT, this);
}; };

View File

@ -0,0 +1,38 @@
'use strict';
// This tests that closing a watcher when the underlying handle is
// already destroyed will result in a noop instead of a crash.
const common = require('../common');
const tmpdir = require('../common/tmpdir');
const fs = require('fs');
const path = require('path');
tmpdir.refresh();
const root = path.join(tmpdir.path, 'watched-directory');
fs.mkdirSync(root);
const watcher = fs.watch(root, { persistent: false, recursive: false });
// The following listeners may or may not be invoked.
watcher.addListener('error', () => {
setTimeout(
() => { watcher.close(); }, // Should not crash if it's invoked
common.platformTimeout(10)
);
});
watcher.addListener('change', () => {
setTimeout(
() => { watcher.close(); },
common.platformTimeout(10)
);
});
fs.rmdirSync(root);
// Wait for the listener to hit
setTimeout(
common.mustCall(() => {}),
common.platformTimeout(100)
);

View File

@ -46,7 +46,8 @@ for (const testCase of cases) {
fs.writeFileSync(testCase.filePath, content1); fs.writeFileSync(testCase.filePath, content1);
let interval; let interval;
const watcher = fs.watch(testCase[testCase.field]); const pathToWatch = testCase[testCase.field];
const watcher = fs.watch(pathToWatch);
watcher.on('error', (err) => { watcher.on('error', (err) => {
if (interval) { if (interval) {
clearInterval(interval); clearInterval(interval);
@ -54,7 +55,11 @@ for (const testCase of cases) {
} }
assert.fail(err); assert.fail(err);
}); });
watcher.on('close', common.mustCall()); watcher.on('close', common.mustCall(() => {
watcher.close(); // Closing a closed watcher should be a noop
// Starting a closed watcher should be a noop
watcher.start();
}));
watcher.on('change', common.mustCall(function(eventType, argFilename) { watcher.on('change', common.mustCall(function(eventType, argFilename) {
if (interval) { if (interval) {
clearInterval(interval); clearInterval(interval);
@ -66,10 +71,16 @@ for (const testCase of cases) {
assert.strictEqual(eventType, 'change'); assert.strictEqual(eventType, 'change');
assert.strictEqual(argFilename, testCase.fileName); assert.strictEqual(argFilename, testCase.fileName);
watcher.start(); // Starting a started watcher should be a noop // Starting a started watcher should be a noop
// End of test case watcher.start();
watcher.start(pathToWatch);
watcher.close(); watcher.close();
// We document that watchers cannot be used anymore when it's closed,
// here we turn the methods into noops instead of throwing
watcher.close(); // Closing a closed watcher should be a noop watcher.close(); // Closing a closed watcher should be a noop
watcher.start(); // Starting a closed watcher should be a noop
})); }));
// Long content so it's actually flushed. toUpperCase so there's real change. // Long content so it's actually flushed. toUpperCase so there's real change.