diff --git a/lib/fs.js b/lib/fs.js index 4fbdaf813018..af6172f70f88 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -3254,6 +3254,11 @@ function realpathSync(p, options) { const seenLinks = new SafeMap(); const knownHard = new SafeSet(); const original = p; + // Whether the symlink this walk resolved last pointed at a pipe or a + // socket, which is where the walk stops. It cannot be read back from the + // shared stat buffer, which holds the last stat made anywhere in the + // process rather than the last one made here. + let reachedPipeOrSocket = false; // Current character position in p let pos; @@ -3297,8 +3302,7 @@ function realpathSync(p, options) { // Continue if not a symlink, break if a pipe/socket if (knownHard.has(base) || cache?.get(base) === base) { - if (isFileType(statValues, S_IFIFO) || - isFileType(statValues, S_IFSOCK)) { + if (reachedPipeOrSocket) { break; } continue; @@ -3336,7 +3340,9 @@ function realpathSync(p, options) { } } if (linkTarget === null) { - binding.stat(base, false, undefined, true); + const targetStats = binding.stat(base, false, undefined, true); + reachedPipeOrSocket = isFileType(targetStats, S_IFIFO) || + isFileType(targetStats, S_IFSOCK); linkTarget = binding.readlink(base, undefined); } resolvedLink = pathModule.resolve(previous, linkTarget); diff --git a/test/parallel/test-fs-realpath-stale-stat-values.js b/test/parallel/test-fs-realpath-stale-stat-values.js new file mode 100644 index 000000000000..5017e6b23467 --- /dev/null +++ b/test/parallel/test-fs-realpath-stale-stat-values.js @@ -0,0 +1,48 @@ +'use strict'; + +// Resolving a path must not depend on what was stat'ed before it. +// +// While walking a path, realpath skips the components it already knows are +// real, and in that branch it consulted the shared stat buffer to decide +// whether the walk had reached a pipe or a socket. That buffer holds the result +// of the last stat made anywhere in the process, so an unrelated stat of a FIFO +// made the walk stop early and hand back the path with its symlinks unresolved. +// The unresolved path is then cached, so every later resolution repeats it. +// +// The walk only takes that branch once something has established the ancestors +// as real, which is the state the module loader's realpath cache is in after it +// has resolved anything else under the same directory. So this goes through +// require() to reach it, and the second copy of the module is what the stale +// read costs. + +const common = require('../common'); + +if (common.isWindows) + common.skip('no mkfifo on Windows'); + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +const pkg = tmpdir.resolve('pkg'); +const link = tmpdir.resolve('pkg-link'); +const fifo = tmpdir.resolve('fifo'); + +fs.mkdirSync(pkg); +fs.writeFileSync(path.join(pkg, 'index.js'), 'module.exports = {};\n'); +fs.writeFileSync(tmpdir.resolve('warm.js'), 'module.exports = {};\n'); +fs.symlinkSync('pkg', link); +execFileSync('mkfifo', [fifo]); + +const throughLink = path.join(link, 'index.js'); +const throughReal = path.join(pkg, 'index.js'); + +require(tmpdir.resolve('warm.js')); +fs.statSync(fifo); + +assert.strictEqual(require.resolve(throughLink), throughReal); +assert.strictEqual(require(throughLink), require(throughReal));