php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #73884 is_dir() returns false for junction / symlinkd
Submitted: 2017-01-06 20:12 UTC Modified: 2017-01-23 10:54 UTC
From: kulakov74 at yandex dot ru Assigned: ab (profile)
Status: Closed Package: Filesystem function related
PHP Version: 7.1.0 OS: Windows 8.1
Private report: No CVE-ID: None
 [2017-01-06 20:12 UTC] kulakov74 at yandex dot ru
Description:
------------
On my Windows PC, I scanned the whole directory structure in C:\ (both from browsers and in CLI mode) and found out that for all of the junctions / symlinks that had been created by the system is_dir() returns false. 

According to the manual, "If filename is a symbolic or hard link then the link will be resolved and checked." but this is only the case with the junctions / symlinks I created manually for testing. It might seem the problem is with permissions but I run the script as admin and also I can see the fact that the items in question are junctions by using plain console (cmd.exe) even not as admin, for ex.: 

c:\Users>dir /a:l
...
22.08.2013  17:45    <SYMLINKD>     All Users [C:\ProgramData]
22.08.2013  17:45    <JUNCTION>     Default User [C:\Users\Default]
...

BTW, In Windows, junctions and symlinks are almost the same. 

Test script:
---------------
$Dir="C:\Users\All Users";
if (is_dir($Dir)) echo("dir"); else echo("not a dir");



Expected result:
----------------
dir

Actual result:
--------------
not a dir

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2017-01-06 21:41 UTC] ab@php.net
Thanks for the report. Might be good a permission issue, yep. Would you mind to investigate more on it, maybe checking what difference the extended permissions tell, etc.?

Thanks.
 [2017-01-07 19:37 UTC] kulakov74 at yandex dot ru
For tests, I tried "Default User", it was owned by System and had a right to read/execute for everyone. After I took over ownership and removed the right, I could not "enter" it and Dir stopped displaying its target (just showed "[..]"). is_dir() still returned false, which is actully not correct because if it has no way to know and check the target, it can't know if it's a dir or not, no error message displayed either. I readded the right to read/execute for everyone and everything works as it did before. 

The junction I created myself (templink) has "read & execute" right for "Users", not for everyone. Also it has Modify permission for Authenticated users. I added both to "Default User" but it didn't help. "Default User" also had "hs" attributes (hidden and system), but after I cleared them nothing changed. The only difference left is that "Default User" has the permissions added to it directly while templink inherits them from C:\. I disabled inheritance for templink but is_dir() still worked for it. 

Still, I could make is_dir() return false for templink by removing all rights. 

So, this could be related with permissions, but I couldn't prove it, and even if I did, php does smth wrong cause it shouldn't cause problems.
 [2017-01-07 20:19 UTC] kulakov74 at yandex dot ru
Note that readlink() returns an error for "c:\Users\Default User\": 

Warning: readlink(): readlink failed to read the symbolic link (C:\Users\Default User), error 5)

I must add that the "Default User" junction does have a Deny rule for everyone to list folder/read data, but in practice it has no effect (if you use any other tool but PHP). After I removed it, both is_dir() and readlink() produced correct results. So it looks like PHP checks the permissions, finds the Deny rule and uses some wrong logic to conclude it has no right to access the link. 

BUT I have another junction also created by me before (not for tests)
c:\Users\Sergei\ pointing to C:\Users\Серёжка (my name in Russian)
and it doesn't have the deny rule, while it has full control for myself, AND is_dir() returns false for it (wrong!), while readlink() returns the right target. So there's some other kind of bug with is_dir().
 [2017-01-08 19:49 UTC] ab@php.net
-Status: Open +Status: Feedback
 [2017-01-08 19:49 UTC] ab@php.net
Thanks for the effort so far. Maybe I expressed myself wrong. I was talking about the NTFS permissions. Either in GUI by viewing advanced security, or by icacls or similar tools. Here is it on my side, with c:\users\all users

php.exe -n -r "$p = 'c:\users\all users'; var_dump(is_dir($p), readlink($p));"
bool(true)
string(14) "C:\ProgramData"

$ icacls "c:\Users\All Users"
c:\Users\All Users NT AUTHORITY\SYSTEM:(OI)(CI)(F)
                   BUILTIN\Administrators:(OI)(CI)(F)
                   CREATOR OWNER:(OI)(CI)(IO)(F)
                   BUILTIN\Users:(OI)(CI)(RX)
                   BUILTIN\Users:(CI)(WD,AD,WEA,WA)


With c:\users\default user - i cannot even view it in the explorer, "access denied" popup. So that's what it gives 

php.exe -n -r "$p = 'c:\users\default user'; var_dump(is_dir($p), readlink($p));"

Warning: readlink(): readlink failed to read the symbolic link (c:\users\default user), error 5) in Command line code on line 1
bool(true)
bool(false)

And that's not a wonder, as 

$ icacls "c:\Users\Default User"
c:\Users\Default User Everyone:(DENY)(S,RD)
                      Everyone:(RX)
                      NT AUTHORITY\SYSTEM:(F)
                      BUILTIN\Administrators:(F)

Given you say, that a custom junction works as expected, but some particular path don't - it still looks like a permission issue. I might be still wrong, though, but really need a reliable reproducer for the case. Which other tools do you mean? Best way would be to have a synthetic case, that creates an fs object with exact access privileges and shows the issue so then it's repeatable on any system.

Thanks.
 [2017-01-08 22:13 UTC] kulakov74 at yandex dot ru
I did many tests and I finally came up with the conclusion the problem is at least in Cyrillic (Russian) characters in directory names (even though I have the latest PHP where the UTF problem has been resolved). I'll give the last lines of my tests for you to make it clear. I'm using russian letters in my tests so make sure you see them. 

I have created 2 directories for tests: 
C:\Temp\Dir
C:\Temp\Кат    (this one is rus)

I'm giving 4 combinations for eng/rus: 

//1. eng -> eng: OK
c:\Temp>mklink /j linktoeng C:\Temp\Dir
Junction created for linktoeng <<===>> C:\Temp\Dir
c:\Temp>php.exe -n -r "$p = 'c:\Temp\linktoeng'; var_dump(is_dir($p), readlink($p));"
bool(true)
string(11) "C:\Temp\Dir"

//2. rus -> eng: OK!
c:\Temp>mklink /j рус-ссылка-наангл C:\Temp\Dir
Junction created for рус-ссылка-наангл <<===>> C:\Temp\Dir
c:\Temp>php.exe -n -r "$p = 'c:\Temp\рус-ссылка-наангл'; var_dump(is_dir($p), readlink($p));"
bool(true)
string(11) "C:\Temp\Dir"

//3. eng -> rus: NO
c:\Temp>mklink /j linktorus C:\Temp\Кат
Junction created for linktorus <<===>> C:\Temp\Кат
c:\Temp>php.exe -n -r "$p = 'c:\Temp\linktorus'; var_dump(is_dir($p), readlink($p));"
bool(false)
string(14) "C:\Temp\Кат"

In this case note that readlink() does perfectly well, also, if I try 
c:\Temp>php.exe -n -r "$p = 'C:\Temp\Кат'; var_dump(is_dir($p), readlink($p));"
bool(true)
string(14) "C:\Temp\Кат"

is_dir() is correct, so it's is_dir()'s only bug and it only shows with links, cause when I give it the russian dir directly it knows it's a dir!

//4. rus -> rus: NO (this is formal, we know it won't work :)
c:\Temp>mklink /j рус-ссылка C:\Temp\Кат
Junction created for рус-ссылка <<===>> C:\Temp\Кат
c:\Temp>php.exe -n -r "$p = 'c:\Temp\рус-ссылка'; var_dump(is_dir($p), readlink($p));"
bool(false)
string(14) "C:\Temp\Кат"

And because I have a russian user name all of the links in my C:\Users fail with PHP. 

//-----------------

As for permissions (ACLs), when you run

icacls "c:\Users\All Users"

as "All Users" is a link icacls displays ACLs for the target, not for the link. If you try 

icacls "c:\programdata"

you'll see the same ACLs, while in fact "All Users" has the same Deny rule you mentioned (I must say all the built-in links have this Deny rule). So you should use

icacls "c:\Users\All Users" /L

>>With c:\users\default user - i cannot even view it in the explorer, "access denied" popup
The same with me :) BUT if I open console (cmd.exe) I can do this: 

dir /A:L C:\Users
...
<JUNCTION>     Default User [C:\Users\Default]
and then I can do
c:\Users>cd "Default User"
c:\Users\Default User>

So, Explorer behaves different with links that have equal ACLs, so this is probably where junctions and symlinks differ. It looks like PHP follows the logic used by Explorer and I don't see why Explorer can't access the objects other tools can. 
                   
Other tools: cmd.exe, Total Commander.
 [2017-01-09 22:48 UTC] ab@php.net
Thanks for the further investigation. I still not reproduce the cases 3. and 4. from your post. Here

$ mkdir bugs\bug73884\Кат

$ icacls bugs\bug73884\Кат
bugs\bug73884\Кат BUILTIN\Administrators:(I)(OI)(CI)(F)
                    NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
                    BUILTIN\Users:(I)(OI)(CI)(RX)
                    NT AUTHORITY\Authenticated Users:(I)(M)
                    NT AUTHORITY\Authenticated Users:(I)(OI)(CI)(IO)(M)


$ mklink /j bugs\bug73884\linktorus bugs\bug73884\Кат
Junction created for bugs\bug73884\linktorus <<===>> bugs\bug73884\Кат

$ icacls bugs\bug73884\linktorus /L
bugs\bug73884\linktorus BUILTIN\Administrators:(I)(OI)(CI)(F)
                        NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
                        BUILTIN\Users:(I)(OI)(CI)(RX)
                        NT AUTHORITY\Authenticated Users:(I)(M)
                        NT AUTHORITY\Authenticated Users:(I)(OI)(CI)(IO)(M)

$ x64\Release\php.exe -n -r "$p = 'bugs\bug73884\linktorus'; var_dump(is_dir($p), readlink($p));"
bool(true)
string(54) "C:\php-sdk\php71\vc14\x64\php-src\bugs\bug73884\Кат"


$ mklink /j bugs\bug73884\рус-ссылка bugs\bug73884\Кат
Junction created for bugs\bug73884\рус-ссылка <<===>> bugs\bug73884\Кат

$ icacls bugs\bug73884\рус-ссылка /L
bugs\bug73884\рус-ссылка BUILTIN\Administrators:(I)(OI)(CI)(F)
                   NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
                   BUILTIN\Users:(I)(OI)(CI)(RX)
                   NT AUTHORITY\Authenticated Users:(I)(M)
                   NT AUTHORITY\Authenticated Users:(I)(OI)(CI)(IO)(M)

$ x64\Release\php.exe -n -r "$p = 'bugs\bug73884\рус-ссылка'; var_dump(is_dir($p), readlink($p));"
bool(true)
string(54) "C:\php-sdk\php71\vc14\x64\php-src\bugs\bug73884\Кат"


Used the current dev tree, of course. Hopefully you used some snapshot after the readlink() fix, too. I'm on win10 however, but not sure it is of importance in this case. Also I don't think the username being not ASCII is relevant. Now see, cmd indeed looks like going into c:\users\default user, doing same as you

$ cd "Default User"

c:\Users\Default User
$ dir
 Volume in drive C is SYSTEM
 Volume Serial Number is AE0A-76BD

 Directory of c:\Users\Default User

File Not Found

But the dir command shows - it cannot read it. Even the dir were empty, it should have shown periods. IMO, what the cd command does, is weird. It in no case looks, like the access no "c:\users\default user" is granted.

The points 3. and 4. are intended to work, otherwise the whole UTF-8 thing were useful. You don't have to explain the russian words to me, anyway ;) The fact is - everything path related is converted to UTF-16 internally, and the corresponding W API is used. Also, PHP doesn't automatically elevate any privileges, it always runs with the default security descriptor, so the one corresponding to the current user. An exception from this the impersonation, in that case the impersonated user identity is used. Thereby, if such an issue is real - it doesn't concern only one particular codepage or language, it is most likely present in any other. For some reason, on machines i've tried this particular case doesn't produce issues you have. Used win 10 and server 2012 both with the cp 437. 

I'd wonder, whether you maybe have some group policies, that force to create files with particular ACLs, etc. Otherwise, it's an awkward situation, as i can't reproduce the issue from your side :( If you're sure, the non ASCII username could be an issue, please try some user with ASCII symbols only. Otherwise, there has to be another factor that we didn't determine yet, causing different behavior.  

Thanks.
 [2017-01-09 22:50 UTC] ab@php.net
Typo, 3. and 4. are intended to work, otherwise UTF-8 were USELESS :)

Thanks.
 [2017-01-10 20:57 UTC] kulakov74 at yandex dot ru
Hm. I'm surprised links to cyrillic dirs worked for you. I thought I found the reason. Well, I can't help now. 

>>The points 3. and 4. are intended to work, otherwise the whole UTF-8 thing were useful.
I know, and readlink() and is_dir() (when applied directly to a cyrillic dir) work fine. But when is_dir() meets a link it has to do an extra step and it may have a bug with buffer length, like the one readlink() had. 

Yeah, I agree Dir seems to go there but it can't show the files there. But it doesn't really matter - is_dir() only has to find out the target dir and "dir /a:l" shows it. So, PHP could get it and check whether it is a dir. But I can't say what Windows API function would return the target like dir does. Anyway, that's a different issue. 

As for cd'ing to the dir, this is what Total Commander does: if I go to "All Users" (it's a symlink), it goes there and the current path is "c:\Users\All Users\", as if it was a folder, although we really get to "C:\ProgramData". And if I go to "Default User" (it's a junction) I get to "c:\Users\Default" - I guess because TC first sniffs the target and then goes to it directly. This is what PHP could do with is_dir(), but without going (cd) there. 

I didn't mean my non-ASCII username was the problem, only non-ASCII dir names, and they could have my name in the path. 
Privileges/ACLs are not the reason here, if only for "Default User" that has a Deny rule, but that's a different issue. 

I use Win 8.1 (x64), the snapshot from 01/07/17. 
I'll also try the latest PHP at office, where I have Win 7 x64. 

Codepage (437) should not matter, as I run the tests from a php script that uses UTF-8. Codepage only matters for cmd.exe, I had 866 and now I have 1251, but that shouldn't matter.
 [2017-01-11 21:49 UTC] ab@php.net
-Status: Feedback +Status: Open
 [2017-01-11 21:49 UTC] ab@php.net
ACK, so i misunderstood the part about the username. There are indeed several places, where the username is relevant, but not this one.

is_dir() is a simple stat() call on the given path.  There's no step in between, so at the very bottom it's really the internal API. Maybe it'd be different if CreateFile and accompanying non POSIX compatible APIs were used, need to experiment with that. I'm leaving this open for now, so someone with a similar issue could possibly deliver more input or better repro way.

With the other issue you've mentioned - please retry putting the code into a file. It is possibly, that the console blows the encoding.

Thanks.
 [2017-01-11 22:51 UTC] ab@php.net
Hmm, but again, 

$ x64\Release\php.exe -n -r "$p = 'c:\\users\\all users'; var_dump(is_dir($p), readlink($p));"
bool(true)
string(14) "C:\ProgramData"

$ x64\Release\php.exe -n -r "$p = 'c:\\users\\default user'; var_dump(is_dir($p), readlink($p));"

Warning: readlink(): readlink failed to read the symbolic link (c:\users\default user), error 5) in Command line code on line 1
bool(true)
bool(false)

Seems I simply come to the start. is_dir() works where i test, and readlink() seems correct according to the ACLs. Please, lets leave the multibyte path aside yet - if there's an issue, it should be handled in a different ticket. So far 7.0 shows same behavior. Need first to reproduce what you've on  your side, which seems to be tricky as i've no machine behaving like that. If someone could produce a synthetic reproduce case, that would give a base to move forward on this. No status for this situation :/

Thanks.
 [2017-01-12 15:15 UTC] kulakov74 at yandex dot ru
>>please retry putting the code into a file
As I wrote I did so. 

I tested with Windows 7 x64 - the bug is still there for both "c:\Users\All Users" and junctions to cyrillic directories. Note that for me is_dir() gives true if the junction itself is cyrillic but the target is latin. The bug shows only if the target is cyrillic, no matter what charset of the link is. 

I also tried the old PHP 5.5 and it gave me correct result with jun2ru (target is rus), but not for All Users. I compared phpinfo() output of both versions and found a difference: in 5.5 default_charset="" while in 7.1 it's UTF-8. I tried using different values for default_charset and finally got correct result with Windows-1251 (Cyrillic):

ini_set('default_charset', 'Windows-1251'); echo(ini_get('default_charset')."\n");
$p='c:\temp\рус2рус'; 
var_dump(is_dir($p), readlink($p));

This way PHP 7.1 was correct, at the same time when I used UTF-8 / no value / iso-8859-1 it was not. 
With PHP 5.5 it seems you can use any default_charset. 

It may be that is_dir() depends on what language is installed in the system, I have Russian installed and for me Windows-1251 worked with PHP 7. 

"c:\Users\All Users\" doesn't work at all in both versions.
 [2017-01-12 15:59 UTC] ab@php.net
Hmm, you previous 4 runs was on console. In the last snippet you show - sure the file is UTF-8 encoded? The hardcoded data have to be in same encoding as the one configured in INI. In PHP before 7.1, the ANSI APIs are used, so the encoding INI doesn't affect anything. I was earlier not able to reproduce issues with any of the 4 runs you've shown earlier, including the ones with multibyte dir/junction, as linked in the post above. To me, it still seems as a permission issue with the specific path on your side.

There's probably no way around, other than me to ask you to please pack an isolated stable repro case. Too much time spent with just shooting in the sky :( 

Thanks.
 [2017-01-12 20:06 UTC] kulakov74 at yandex dot ru
>>sure the file is UTF-8 encoded? 
Yes, but it doesn't even matter cause the point is in the targets' charset. 

Tested again on Win 8.1, and the trick with default_charset=Windows-1251 worked again :)
 [2017-01-12 21:50 UTC] ab@php.net
Oh, that's interesting. That's a good spot then. Were some how new to me. Or more precisely - of course, it's possible to use another encoding, weird is it's done on system supplied objects :/ Any FS objects created/read by PHP or other programs correctly handling Unicode, is fine with 7.1. Your workaround is actually documented in UPGRADING ;) - if there's need to work with incompatible items, etc., normally configuring for the system codepages would be fine.

So is_dir() and other works as expceted, when you set another codepage? In that case, the issue might be resolved.

Thanks.
 [2017-01-21 11:22 UTC] ab@php.net
-Status: Open +Status: Feedback
 [2017-01-21 11:22 UTC] ab@php.net
@kulakov74, may I ask you to check the latest snapshots again? The fix for bug #73962 might be related to this issue.

Thanks.
 [2017-01-21 12:55 UTC] kulakov74 at yandex dot ru
I tried "PHP 7.1.2-dev (cli) (built: Jan 21 2017 08:06:44)", the issue with links to cyrilic targets is now solved for UTF-8 as well (it did work correct for Windows-1251, not it works for both).
 [2017-01-23 10:54 UTC] ab@php.net
-Status: Feedback +Status: Closed -Assigned To: +Assigned To: ab
 [2017-01-23 10:54 UTC] ab@php.net
Great! So it was same issue, just a more lucky reproducer. NTFS always handles filenames in UTF-16, the OS created links are no diff and was suffering from the bug in the realpath conversion, not because they were differently encoded.

Thanks.
 
PHP Copyright © 2001-2018 The PHP Group
All rights reserved.
Last updated: Wed Sep 19 16:01:27 2018 UTC