php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #69473 bug in symlink() prevents relative symlinks on Windows
Submitted: 2015-04-16 20:29 UTC Modified: 2015-04-20 07:50 UTC
Votes:4
Avg. Score:5.0 ± 0.0
Reproduced:4 of 4 (100.0%)
Same Version:0 (0.0%)
Same OS:3 (75.0%)
From: email at spikx dot net Assigned: pajoye
Status: Assigned Package: Filesystem function related
PHP Version: 5.6.8 OS: Windows 7
Private report: No CVE-ID:
Have you experienced this issue?
Rate the importance of this bug to you:

 [2015-04-16 20:29 UTC] email at spikx dot net
Description:
------------
As far as I checked, the following applies to any PHP version since the symlink function was introduced (in PHP 5.3).

Currently it is impossible under Windows to create relative symbolic links with PHP's symlink function. It will always fail with the message "Could not fetch file information". The reason is the following:

At line #169 ( https://github.com/php/php-src/blob/master/ext/standard/link_win32.c#L169 ) the function checks, if the $target ("topath" in the C function) actually exists:

if ((attr = GetFileAttributes(topath)) == INVALID_FILE_ATTRIBUTES) { ... }

However, the Win32API function "GetFileAttributes" expects the path to be either absolute or relative **to the CWD**! This means that this check will always fail, if you want to use a relative symlink, since the relative path of the symlink is relative to the $source's directory, not relative to the CWD.

Test script:
---------------
<?php

mkdir('foo1');
mkdir('foo2');

symlink( '../foo2', 'foo1/foo3' );


Expected result:
----------------
/foo1 should contain a directory symlink to ../foo2 (i.e. the /foo2 directory in the root folder)

Actual result:
--------------
Under Windows:

Warning: symlink(): Could not fetch file information(error 2) in index.php on line 6


Under Linux:

foo1 contains a directory symlink to ../foo2 (i.e. the foo2 directory in CWD)

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-04-17 07:08 UTC] email at spikx dot net
The issue with the CWD is even more problematic than expected. Assume you have a local XAMPP installation on your Windows system and the aforementioned script is under C:\xampp\htdocs\symlinktest\index.php

Now, if you execute this script via Apache (e.g. by opening it via your Browser), the CWD (for the Win32API functions) will actually be

C:\xampp (!)

At least it was in my case. If you execute the script via the command line directly within C:\xampp\htdocs\symlinktest, then the CWD for the Win32API functions will also be C:\xampp\htdocs\symlinktest as expected.

Calling chdir(…) before the symlink(…) call did not change the CWD for the Win32API function as well.


In my opinion, the 

if ((attr = GetFileAttributes(topath)) == INVALID_FILE_ATTRIBUTES) { ... }

should simply be removed - or only applied, if `topath` is not a relative path. It's then the responsibility of the user to make sure the relative path provided in `$target` is valid/meaningful. As it is currently, PHP's symlink function only allows you to generate invalid relative symlinks anyway.
 [2015-04-17 13:49 UTC] pajoye@php.net
Your script creates a link to a non existing directory (../foo2) instead of the one you created (./foo2).

Or am I missing something?
 [2015-04-17 16:09 UTC] email at spikx dot net
No, the symlink, which is called "foo3" in this case is located in the folder foo2 ($source is 'foo2/foo3') and points to '../foo1' ($target), i.e. the foo1 folder in the root directory (which is the parent directory of foo2). Look at this console output from Linux, where it works, I think it will get more clear then:

[...]/symlinktest # vi index.php (create the aformentioned script)
[...]/symlinktest # php index.php (execute script via php binary)
[...]/symlinktest # ls -la (list root directory)

4.0K Apr 17 18:02 .
4.0K Apr 17 18:01 ..
4.0K Apr 17 18:02 foo1
4.0K Apr 17 18:02 foo2
  71 Apr 17 18:02 index.php

[...]/symlinktest # cd foo2
[...]/symlinktest/foo2 # ls -la

4.0K Apr 17 18:02 .
4.0K Apr 17 18:02 ..
   7 Apr 17 18:02 foo3 -> ../foo1


Just to mention it again: relative symlinks work on Windows too. However, PHP's implementation of the symlink function prevents relative symlinks, due to the GetFileAttributes call on the target path.
 [2015-04-17 16:13 UTC] email at spikx dot net
Sorry, I mixed up the foo1 and foo2 folders in this test (I created a symlink to ../foo1 within foo2 instead of a symlink to ../foo2 within foo1, like in the original example), but you get the idea, doesn't matter anyway :)
 [2015-04-18 03:22 UTC] pajoye@php.net
Your cwd is not foo1, so it won't work

See https://msdn.microsoft.com/en-us/library/windows/desktop/aa363878%28v=vs.85%29.aspx
 [2015-04-18 08:43 UTC] email at spikx dot net
No, relative symbolic links do not need to be CWD relative. See the first point in your linked documentation:

Relative links are specified using the following conventions:

* Dot (. and ..) conventions—for example, "..\" resolves the path relative to the parent directory.
[...]

As already said: relative symbolic links work the same way in Windows as they do under Linux. The script works under Linux and it would work under Windows as well, if the symlink function of PHP did not use GetFileAttributes to check if the $target exists - since it checks a completely wrong path.

Just try it yourself under Windows. As already said, this will not work:

<?php

mkdir('foo1');
mkdir('foo2');

symlink( '../foo2', 'foo1/foo3' );


However, THIS will work:

<?php

mkdir('foo1');
mkdir('foo2');

symlink( '../symlinktest/foo2', 'foo1/foo3' );

This works, because now the GetFileAttributes receives the path '../symlinktest/foo2', which is a valid path relative to the CWD. However it creates an invalid symlink, since it is a relative symlnk, which resides under /symlinktest/foo1, and will now point to '../symlinktest/foo2', which does not exist (the full expanded path is 'C:\xampp\htdocs\symlinktest\symlinktest\foo2').
 [2015-04-18 10:16 UTC] cmb@php.net
From the symlink(2) man page[1]:

| Symbolic links may contain .. path components, which (if used at
| the start of the link) refer to the parent directories of that in
| which the link resides.

This is even acknowledged by a comment in the code[2]:

| The target is relative to the link itself, not to the CWD.

However, it doesn't behave that way.

[1] <http://man7.org/linux/man-pages/man2/symlink.2.html>
[2] <https://github.com/php/php-src/blob/master/ext/standard/link_win32.c#L176>
 [2015-04-18 11:53 UTC] email at spikx dot net
cmb@php.net: exactly - it's just the $target check [1] right before the pCreateSymbolicLinkA call that prevents using relative symlinks, since the GetFileAttributes expects the path to be either absolute or relative to the CWD, not relative to the parent directories of that in which the link to the target resides.

[1] <https://github.com/php/php-src/blob/master/ext/standard/link_win32.c#L169>
 [2015-04-18 12:44 UTC] cmb@php.net
-Status: Open +Status: Assigned -Assigned To: +Assigned To: cmb
 [2015-04-18 12:44 UTC] cmb@php.net
> it's just the $target check

Apparently, dest_p has to be checked there instead of topath.
I'm working on a respective PR.
 [2015-04-18 14:44 UTC] cmb@php.net
-Package: Win32API related +Package: Filesystem function related -Assigned To: cmb +Assigned To: pajoye
 [2015-04-18 14:44 UTC] cmb@php.net
I've submitted the PR <https://github.com/php/php-src/pull/1243>.

Pierre, can you please have a look at it.
 [2015-04-20 07:45 UTC] ab@php.net
The main concern I'd like to express about this is the thread safety. A robust solution should include CWD magic when relative paths are used. Otherwise it might make sense to rework this for CLI or NTS only.

What @spikx means about chdir in PHP looks like confused with _chdir in CRT. Two completely unrelated things, so chdir() in PHP can't affect anything in the symlink behavior.

Thanks.
 [2015-04-20 07:50 UTC] ab@php.net
see also bug #68167
 
PHP Copyright © 2001-2017 The PHP Group
All rights reserved.
Last updated: Tue Aug 29 15:01:52 2017 UTC