GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — test: cover `realpathSync` resolving symlinks after a FIFO stat · nodejs/node@87ee7bf

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • lib
    • fs.js
  • test/parallel
    • test-fs-realpath-async-stale-stat-values.js
    • test-fs-realpath-stale-stat-values.js
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎lib/fs.js‎

Copy file name to clipboardExpand all lines: lib/fs.js
+18-7Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ const {
8181

8282
const {
8383
FSReqCallback,
84-
statValues,
8584
} = binding;
8685
const { toPathIfFileURL } = require('internal/url');
8786
const {
@@ -3254,6 +3253,11 @@ function realpathSync(p, options) {
32543253
const seenLinks = new SafeMap();
32553254
const knownHard = new SafeSet();
32563255
const original = p;
3256+
// Whether the symlink this walk resolved last pointed at a pipe or a
3257+
// socket, which is where the walk stops. It cannot be read back from the
3258+
// shared stat buffer, which holds the last stat made anywhere in the
3259+
// process rather than the last one made here.
3260+
let reachedPipeOrSocket = false;
32573261

32583262
// Current character position in p
32593263
let pos;
@@ -3297,8 +3301,7 @@ function realpathSync(p, options) {
32973301

32983302
// Continue if not a symlink, break if a pipe/socket
32993303
if (knownHard.has(base) || cache?.get(base) === base) {
3300-
if (isFileType(statValues, S_IFIFO) ||
3301-
isFileType(statValues, S_IFSOCK)) {
3304+
if (reachedPipeOrSocket) {
33023305
break;
33033306
}
33043307
continue;
@@ -3336,7 +3339,9 @@ function realpathSync(p, options) {
33363339
}
33373340
}
33383341
if (linkTarget === null) {
3339-
binding.stat(base, false, undefined, true);
3342+
const targetStats = binding.stat(base, false, undefined, true);
3343+
reachedPipeOrSocket = isFileType(targetStats, S_IFIFO) ||
3344+
isFileType(targetStats, S_IFSOCK);
33403345
linkTarget = binding.readlink(base, undefined);
33413346
}
33423347
resolvedLink = pathModule.resolve(previous, linkTarget);
@@ -3418,6 +3423,11 @@ function realpath(p, options, callback) {
34183423

34193424
const seenLinks = new SafeMap();
34203425
const knownHard = new SafeSet();
3426+
// Whether the symlink this walk resolved last pointed at a pipe or a
3427+
// socket, which is where the walk stops. It cannot be read back from the
3428+
// shared stat buffer, which holds the last stat made anywhere in the
3429+
// process rather than the last one made here.
3430+
let reachedPipeOrSocket = false;
34213431

34223432
// Current character position in p
34233433
let pos;
@@ -3466,8 +3476,7 @@ function realpath(p, options, callback) {
34663476

34673477
// Continue if not a symlink, break if a pipe/socket
34683478
if (knownHard.has(base)) {
3469-
if (isFileType(statValues, S_IFIFO) ||
3470-
isFileType(statValues, S_IFSOCK)) {
3479+
if (reachedPipeOrSocket) {
34713480
return callback(null, encodeRealpathResult(p, options));
34723481
}
34733482
return process.nextTick(LOOP);
@@ -3497,9 +3506,11 @@ function realpath(p, options, callback) {
34973506
return gotTarget(null, seenLinks.get(id));
34983507
}
34993508
}
3500-
fs.stat(base, (err) => {
3509+
fs.stat(base, (err, targetStats) => {
35013510
if (err) return callback(err);
35023511

3512+
reachedPipeOrSocket = targetStats.isFIFO() || targetStats.isSocket();
3513+
35033514
fs.readlink(base, (err, target) => {
35043515
if (!isWindows) seenLinks.set(id, target);
35053516
gotTarget(err, target);
Collapse file

‎test/parallel/test-fs-realpath-async-stale-stat-values.js‎

Copy file name to clipboard
+62Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict';
2+
3+
// The async realpath() reads the shared stat buffer the same way realpathSync()
4+
// did, to decide whether the walk has reached a pipe or a socket. The walk's
5+
// own fs.stat() does leave the right value there, but it is not read until
6+
// after fs.readlink() and a process.nextTick(), and any stat completing in that
7+
// window replaces it.
8+
//
9+
// Truncating the walk only costs something when a second symlink follows the
10+
// one being resolved, so the path used here has two.
11+
12+
const common = require('../common');
13+
14+
if (common.isWindows)
15+
common.skip('no mkfifo on Windows');
16+
17+
const assert = require('assert');
18+
const fs = require('fs');
19+
const path = require('path');
20+
const { execFileSync } = require('child_process');
21+
const tmpdir = require('../common/tmpdir');
22+
23+
tmpdir.refresh();
24+
25+
const real = tmpdir.resolve('real');
26+
const pkg = tmpdir.resolve('pkg');
27+
const fifo = tmpdir.resolve('fifo');
28+
29+
fs.mkdirSync(real);
30+
fs.mkdirSync(pkg);
31+
fs.writeFileSync(path.join(real, 'index.js'), '');
32+
fs.symlinkSync(path.join('..', 'real'), path.join(pkg, 'sub'));
33+
fs.symlinkSync('pkg', tmpdir.resolve('link'));
34+
execFileSync('mkfifo', [fifo]);
35+
36+
const throughLinks = tmpdir.resolve('link', 'sub', 'index.js');
37+
const expected = path.join(real, 'index.js');
38+
39+
// Keep stats of the FIFO completing for as long as the walk runs, so that one
40+
// of them lands in the buffer during the window.
41+
let settled = false;
42+
(function statFifo() {
43+
if (settled) return;
44+
fs.stat(fifo, statFifo);
45+
})();
46+
47+
let error;
48+
let resolvedPath;
49+
50+
fs.realpath(throughLinks, common.mustCall((err, resolved) => {
51+
settled = true;
52+
error = err;
53+
resolvedPath = resolved;
54+
}));
55+
56+
// Asserted on exit rather than in the callback. An assertion that fails inside
57+
// this callback is lost: it does not reach an `uncaughtException` handler and
58+
// the process still exits 0, so the test would pass over the bug it covers.
59+
process.on('exit', () => {
60+
assert.ifError(error);
61+
assert.strictEqual(resolvedPath, expected);
62+
});
Collapse file

‎test/parallel/test-fs-realpath-stale-stat-values.js‎

Copy file name to clipboard
+73Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
4+
// Resolving a path must not depend on what was stat'ed before it.
5+
//
6+
// While walking a path, realpath skips the components it already knows are
7+
// real, and in that branch it consulted the shared stat buffer to decide
8+
// whether the walk had reached a pipe or a socket. That buffer holds the result
9+
// of the last stat made anywhere in the process, so an unrelated stat of a FIFO
10+
// made the walk stop early and hand back the path with its symlinks unresolved.
11+
// The unresolved path is then cached, so every later resolution repeats it.
12+
13+
const common = require('../common');
14+
15+
if (common.isWindows)
16+
common.skip('no mkfifo on Windows');
17+
18+
const assert = require('assert');
19+
const fs = require('fs');
20+
const path = require('path');
21+
const { execFileSync } = require('child_process');
22+
const { realpathCacheKey } = require('internal/fs/utils');
23+
const tmpdir = require('../common/tmpdir');
24+
25+
tmpdir.refresh();
26+
27+
const pkg = tmpdir.resolve('pkg');
28+
const link = tmpdir.resolve('pkg-link');
29+
const fifo = tmpdir.resolve('fifo');
30+
31+
fs.mkdirSync(pkg);
32+
fs.writeFileSync(path.join(pkg, 'index.js'), 'module.exports = {};\n');
33+
fs.writeFileSync(tmpdir.resolve('warm.js'), 'module.exports = {};\n');
34+
fs.symlinkSync('pkg', link);
35+
execFileSync('mkfifo', [fifo]);
36+
37+
const throughLink = path.join(link, 'index.js');
38+
const throughReal = path.join(pkg, 'index.js');
39+
40+
// The walk only skips a component once something has established it as real. A
41+
// cache carrying the ancestors is that state, and it is the state the module
42+
// loader's own cache is in after it has resolved anything else under the
43+
// directory.
44+
function ancestorCache() {
45+
const cache = new Map();
46+
let dir = '';
47+
for (const part of tmpdir.path.split(path.sep).slice(1)) {
48+
dir += path.sep + part;
49+
cache.set(dir, dir);
50+
}
51+
return cache;
52+
}
53+
54+
fs.statSync(path.join(pkg, 'index.js'));
55+
assert.strictEqual(
56+
fs.realpathSync(throughLink, { [realpathCacheKey]: ancestorCache() }),
57+
throughReal,
58+
);
59+
60+
fs.statSync(fifo);
61+
assert.strictEqual(
62+
fs.realpathSync(throughLink, { [realpathCacheKey]: ancestorCache() }),
63+
throughReal,
64+
);
65+
66+
// What the stale read costs through the module loader, whose cache puts the
67+
// walk in that same state: the symlink stays unresolved, so the file is loaded
68+
// a second time under a second name.
69+
require(tmpdir.resolve('warm.js'));
70+
fs.statSync(fifo);
71+
72+
assert.strictEqual(require.resolve(throughLink), throughReal);
73+
assert.strictEqual(require(throughLink), require(throughReal));

0 commit comments

Comments
0 (0)
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.