@@ -733,6 +733,29 @@ static inline char *instance_to_template(const char *instname, int len)
733733/*
734734Returns new instance path or NULL if template path is OK
735735filename is relative to template root
736+
737+ CRITICAL FIX FOR SPL ITERATORS (BR-12610):
738+ ===========================================
739+
740+ Problem: RecursiveDirectoryIterator fails with "Failed to open directory"
741+ when Shadow incorrectly treats a FILE as a DIRECTORY.
742+
743+ Example failure:
744+ new RecursiveDirectoryIterator('api')
745+ -> Iterates and finds "rest.php" file
746+ -> Calls hasChildren() -> is_dir('api/rest.php')
747+ -> is_dir() uses CACHED path without type validation
748+ -> Returns TRUE (file exists, but not validated as directory!)
749+ -> getChildren() tries to open FILE as directory
750+ -> FATAL ERROR
751+
752+ Solution:
753+ When OPT_CHECK_EXISTS is set:
754+ 1. Use stat() instead of just access() check
755+ 2. Validate S_ISDIR() vs S_ISREG() vs S_ISLNK()
756+ 3. Store type metadata in cache
757+ 4. Only cache as directory if it IS a directory
758+ 5. This prevents files from ever being treated as directories
736759*/
737760static char * template_to_instance (const char * filename , int options )
738761{
@@ -758,7 +781,7 @@ static char *template_to_instance(const char *filename, int options)
758781
759782 if (is_subdir_of (ZSTR_VAL (SHADOW_G (template )), ZSTR_LEN (SHADOW_G (template )), realpath , fnamelen )) {
760783 if (SHADOW_G (debug ) & SHADOW_DEBUG_PATHCHECK ) fprintf (stderr , "In template: %s\n" , realpath );
761- if ((options & OPT_CHECK_EXISTS ) && shadow_cache_get (realpath , & newname ) == SUCCESS ) {
784+ if ((options & OPT_CHECK_EXISTS ) && shadow_cache_get (realpath , & newname , options ) == SUCCESS ) {
762785 if (SHADOW_G (debug ) & SHADOW_DEBUG_PATHCHECK ) fprintf (stderr , "Path check from cache: %s => %s\n" , realpath , newname );
763786 if (realpath ) {
764787 efree (realpath );
@@ -776,15 +799,36 @@ static char *template_to_instance(const char *filename, int options)
776799 if ((options & OPT_CHECK_EXISTS )
777800 && !instance_only_subdir (realpath + ZSTR_LEN (SHADOW_G (template )) + 1 )
778801 ) {
779- if (VCWD_ACCESS (newname , F_OK ) != 0 ) {
780- /* file does not exist */
802+ /*
803+ * CRITICAL FIX: Use stat() instead of access() to get file type.
804+ * This prevents files from being cached as valid directory paths.
805+ */
806+ struct stat st ;
807+ if (VCWD_STAT (newname , & st ) != 0 ) {
808+ /* file/directory does not exist */
781809 efree (newname );
782810 newname = NULL ;
811+ } else {
812+ /* File exists - store with appropriate type marker */
813+ uint32_t cache_type = SHADOW_CACHE_TYPE_UNKNOWN ;
814+ if (S_ISREG (st .st_mode )) {
815+ cache_type = SHADOW_CACHE_TYPE_FILE ;
816+ } else if (S_ISDIR (st .st_mode )) {
817+ cache_type = SHADOW_CACHE_TYPE_DIR ;
818+ } else if (S_ISLNK (st .st_mode )) {
819+ cache_type = SHADOW_CACHE_TYPE_LINK ;
820+ }
821+ if (!(options & OPT_SKIP_CACHE )) {
822+ shadow_cache_put (realpath , newname , cache_type );
823+ }
783824 }
784825 /* drop down to return */
785- }
786- if (!(options & OPT_SKIP_CACHE )) {
787- shadow_cache_put (realpath , newname );
826+ } else if (!(options & OPT_SKIP_CACHE )) {
827+ /*
828+ * No existence check requested - cache without type validation.
829+ * This maintains backward compatibility for write operations.
830+ */
831+ shadow_cache_put (realpath , newname , SHADOW_CACHE_TYPE_UNKNOWN );
788832 }
789833 } else if (is_subdir_of (ZSTR_VAL (SHADOW_G (instance )), ZSTR_LEN (SHADOW_G (instance )), realpath , fnamelen )) {
790834 if (SHADOW_G (debug ) & SHADOW_DEBUG_PATHCHECK ) fprintf (stderr , "In instance: %s\n" , realpath );
0 commit comments