@@ -16,8 +16,8 @@ const {
1616 StringPrototypeEndsWith,
1717} = primordials ;
1818
19- const { lstatSync, readdirSync, statSync } = require ( 'fs' ) ;
20- const { lstat, readdir, stat } = require ( 'fs/promises' ) ;
19+ const { lstatSync, readdirSync, statSync, realpathSync } = require ( 'fs' ) ;
20+ const { lstat, readdir, stat, realpath } = require ( 'fs/promises' ) ;
2121const { join, resolve, basename, isAbsolute, dirname } = require ( 'path' ) ;
2222
2323const {
@@ -134,6 +134,7 @@ class Cache {
134134 #statsCache = new SafeMap ( ) ;
135135 #readdirCache = new SafeMap ( ) ;
136136 #followSymlinks = false ;
137+ #visitedRealpaths = new SafeSet ( ) ;
137138
138139 setFollowSymlinks ( followSymlinks ) {
139140 this . #followSymlinks = followSymlinks ;
@@ -143,6 +144,14 @@ class Cache {
143144 return this . #followSymlinks;
144145 }
145146
147+ hasVisitedRealPath ( path ) {
148+ return this . #visitedRealpaths. has ( path ) ;
149+ }
150+
151+ addVisitedRealPath ( path ) {
152+ this . #visitedRealpaths. add ( path ) ;
153+ }
154+
146155 stat ( path ) {
147156 const cached = this . #statsCache. get ( path ) ;
148157 if ( cached ) {
@@ -457,18 +466,33 @@ class Glob {
457466 for ( let i = 0 ; i < children . length ; i ++ ) {
458467 const entry = children [ i ] ;
459468 const entryPath = join ( path , entry . name ) ;
469+ const entryFullPath = join ( fullpath , entry . name ) ;
460470
461471 // If followSymlinks is enabled and entry is a symlink, resolve it
462472 let resolvedEntry = entry ;
473+ let resolvedRealpath ;
463474 if ( this . #cache. isFollowSymlinks ( ) && entry . isSymbolicLink ( ) ) {
464- const resolved = this . #cache. statSync ( join ( fullpath , entry . name ) ) ;
475+ const resolved = this . #cache. statSync ( entryFullPath ) ;
465476 if ( resolved && ! resolved . isSymbolicLink ( ) ) {
466477 resolvedEntry = resolved ;
467478 resolvedEntry . name = entry . name ;
479+ try {
480+ resolvedRealpath = realpathSync ( entryFullPath ) ;
481+ } catch {
482+ // broken symlink or permission issue – fall back to lstat view
483+ }
484+ }
485+ }
486+
487+ // Guard against cycles when following symlinks into directories
488+ if ( resolvedRealpath && resolvedEntry . isDirectory ( ) ) {
489+ if ( this . #cache. hasVisitedRealPath ( resolvedRealpath ) ) {
490+ continue ;
468491 }
492+ this . #cache. addVisitedRealPath ( resolvedRealpath ) ;
469493 }
470494
471- this . #cache. addToStatCache ( join ( fullpath , entry . name ) , resolvedEntry ) ;
495+ this . #cache. addToStatCache ( entryFullPath , resolvedEntry ) ;
472496
473497 const subPatterns = new SafeSet ( ) ;
474498 const nSymlinks = new SafeSet ( ) ;
@@ -678,18 +702,33 @@ class Glob {
678702 for ( let i = 0 ; i < children . length ; i ++ ) {
679703 const entry = children [ i ] ;
680704 const entryPath = join ( path , entry . name ) ;
705+ const entryFullPath = join ( fullpath , entry . name ) ;
681706
682707 // If followSymlinks is enabled and entry is a symlink, resolve it
683708 let resolvedEntry = entry ;
709+ let resolvedRealpath ;
684710 if ( this . #cache. isFollowSymlinks ( ) && entry . isSymbolicLink ( ) ) {
685- const resolved = await this . #cache. stat ( join ( fullpath , entry . name ) ) ;
711+ const resolved = await this . #cache. stat ( entryFullPath ) ;
686712 if ( resolved && ! resolved . isSymbolicLink ( ) ) {
687713 resolvedEntry = resolved ;
688714 resolvedEntry . name = entry . name ;
715+ try {
716+ resolvedRealpath = await realpath ( entryFullPath ) ;
717+ } catch {
718+ // broken symlink or permission issue – fall back to lstat view
719+ }
720+ }
721+ }
722+
723+ // Guard against cycles when following symlinks into directories
724+ if ( resolvedRealpath && resolvedEntry . isDirectory ( ) ) {
725+ if ( this . #cache. hasVisitedRealPath ( resolvedRealpath ) ) {
726+ continue ;
689727 }
728+ this . #cache. addVisitedRealPath ( resolvedRealpath ) ;
690729 }
691730
692- this . #cache. addToStatCache ( join ( fullpath , entry . name ) , resolvedEntry ) ;
731+ this . #cache. addToStatCache ( entryFullPath , resolvedEntry ) ;
693732
694733 const subPatterns = new SafeSet ( ) ;
695734 const nSymlinks = new SafeSet ( ) ;
0 commit comments