php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #35791 file_exists() does not see removed symlinks
Submitted: 2005-12-23 22:16 UTC Modified: 2006-01-06 02:41 UTC
Votes:1
Avg. Score:1.0 ± 0.0
Reproduced:0 of 1 (0.0%)
From: jgmtfia at gmail dot com Assigned:
Status: Wont fix Package: Filesystem function related
PHP Version: 5CVS-2006-01-03 (snap) OS: Linux
Private report: No CVE-ID: None
Have you experienced this issue?
Rate the importance of this bug to you:

 [2005-12-23 22:16 UTC] jgmtfia at gmail dot com
Description:
------------
I have narrowed down a PHP problem.  I can demonstrate it with file_exists().

I have 2 files, A and B, and one symlink C that points to A.
    -rw-r--r--  Dec 23 14:13 A
    -rw-r--r--  Dec 23 14:13 B
    lrwxr-xr-x  Dec 23 14:13 C -> A

When I delete link C from PHP, and then clearstatcache(), file_exists() returns true for link C.  **It continues to do so until A is removed from disk**.  Using the Apache2 module, I do multiple page reloads and only after some time has passed does PHP notice that link C has been removed.

Not a big problem in general, however, if I do the following:
  unlink("C");
  symlink("B", "C");
  file_get_contents("C");
**PHP still reads the contents of A**.  Using the CLI version I have to run the script again before the contents of B are read.  In Apache I need to reload multiple times.

I can reproduce this on linux kernels 2.6.10 and 2.6.14.  On ext2 and ext3 filesystems.  The code below executes correctly (passes) with PHP 4.3.10 and fails on 5.1.0b3, 5.1.0 and 5.1.1.

Reproduce code:
---------------
<?php
`touch A; ln -s A C`;
$FILE = 'C';
$LS = "ls -log $FILE 2>&1";
clearstatcache();

if(!file_exists($FILE))
    echo "$FILE link does not exist, cannot run test.\n";

echo "File '$FILE' exists:\n\t\t".`$LS`."\n";

if(!unlink($FILE)){
    echo "Unable to delete '$FILE'\n\t\t".`$LS`."\n";
    exit;
}

clearstatcache();

echo "Deleted '$FILE'\n\t\t".`$LS`."\n";
if(!file_exists($FILE)){
    echo "(PASS) PHP sees the file as deleted.\n";
    exit;
}

echo "(FAIL) File '$FILE' deleted as seen by OS.  ";
echo "file_exists() still thinks it exists:\n\t\t".`$LS`."\n";

echo "Deleting 'A'\n";
unlink('A');
clearstatcache();

if(file_exists($FILE)){
    echo "(FAIL) PHP still sees '$FILE' as existing:\n\t\t".`$LS`."\n";
    exit;
}

echo "(FAIL) PHP sees file as deleted only after link target deleted.\n";
?>


Expected result:
----------------
~/x$ ./php-4.3.10-15 ./test.php
File 'C' exists:
                lrwxr-xr-x  1 1 Dec 23 14:00 C -> A

Deleted 'C'
                ls: C: No such file or directory

(PASS) PHP sees the file as deleted.


Actual result:
--------------
~/x$ ./php-5.1.1 ./test.php
File 'C' exists:
                lrwxr-xr-x  1 1 Dec 23 14:02 C -> A

Deleted 'C'
                ls: C: No such file or directory

(FAIL) File 'C' deleted as seen by OS.  file_exists() still thinks it 
exists:
                ls: C: No such file or directory

Deleting 'A'
(FAIL) PHP sees file as deleted only after link target deleted.


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2005-12-24 00:23 UTC] tony2001@php.net
That's what I get:

File 'C' exists:
rwxrwxrwx  1 1 2005-12-24 02:22 C -> A
Deleted 'C'
ls: C: No such file or directory
(PASS) PHP sees the file as deleted.
 [2005-12-24 01:07 UTC] judas dot iscariote at gmail dot com
File 'C' exists:
                lrwxrwxrwx  1 1 2005-12-23 21:10 C -> A

Deleted 'C'
                ls: C: No such file or directory

(PASS) PHP sees the file as deleted.

php -v 
PHP 5.1.2RC2-dev (cli) (built: Dec 23 2005 17:37:05)

SUSE Linux 10 x86_64
 [2006-01-02 17:10 UTC] jgmtfia at gmail dot com
I tested the "lastest" version (php5.1-200601021330) on debian stable and it failed.  Over the holidays I tried 5.1.1 with debian testing and it passed.

A summary of results:
4.3.10 - debian stable - pass
5.1.0b3 - debian stable - fail
5.1.0 - debian stable - fail
5.1.1 - debian stable - fail
5.1.1 - debian testing - pass
php5.1-200601021330 - debian stable - fail
5.1.2RC2-dev - SUSE Linux 10 x86_64 - pass

Common to all failures is Debian stable.  Do you have any suggestions on how to approach finding what is causing the fault?
 [2006-01-02 17:15 UTC] iliaa@php.net
You can strace the script and see what files are actually being accessed.
 [2006-01-03 17:19 UTC] jgmtfia at gmail dot com
I want to ensure that I have made my point clear.  I have made a simplier example, and have included an edited strace of PHP built from yesterdays source.

The test:
<?php
`touch A; ln -s A C`;

echo "Checking A.\n";
if(!file_exists('A')){
    echo "A does not exist.\n";
    exit;
}

echo "Checking C.\n";
if(!file_exists('C')){
    echo "C does not exist.\n";
    exit;
}

echo "Unlinking C.\n";
unlink('C');

clearstatcache();

echo "Checking C: ";
if(file_exists('C'))
    echo "(FAIL) exists\n";
?>

The ouput: 
Checking A.
Checking C.
Unlinking C.
Checking C: (FAIL) exists

The edited strace:
write(1, "Checking A.\n", 12)
lstat64("/home", ...) = 0
lstat64("/home/user", ...) = 0
lstat64("/home/user/x", ...) = 0
lstat64("/home/user/x/A", ...) = 0
access("/home/user/x/A", F_OK) = 0

write(1, "Checking C.\n", 12)
lstat64("/home", ...) = 0
lstat64("/home/user", = 0
lstat64("/home/user/x", = 0
lstat64("/home/user/x/C", = 0
readlink("/home/user/x/C", "A", 4096) = 1
lstat64("/home/user/x/A", ...) = 0
access("/home/user/x/A", F_OK) = 0

write(1, "Unlinking C.\n", 13) = 13
unlink("/home/user/x/C") = 0
write(1, "Checking C: ", 12) = 12
***ERROR SHOULD BE "/home/user/x/C" BELOW ***
access("/home/user/x/A", F_OK) = 0
***END***
write(1, "(FAIL) exists\n", 14) = 14

So the question is why is PHP calling access("/home/user/x/A") when the code calls file_exists('C')?

Also note that if the file_exists('C') call is removed from the start code, the code then executes correctly.
<?php
`touch A; ln -s A C`;

echo "Unlinking C.\n";
unlink('C');

clearstatcache();

echo "Checking C: ";
if(file_exists('C'))
    echo "(FAIL) exists\n";

?>

I don't know if it would be the operating system that would cause PHP to access("/home/user/x/A") when I call file_exists('C').

debain php4.3.10-15 does pass the test and there are no lstat64 or readlink calls.

write(1, "Checking A.\n", 12) = 12
access("A", F_OK) = 0
write(1, "Checking C.\n", 12) = 12
access("C", F_OK) = 0
write(1, "Unlinking C.\n", 13) = 13
unlink("C") = 0
write(1, "Checking C: ", 12) = 12
access("C", F_OK) = -1 ENOENT (No such file or directory)
 [2006-01-03 18:31 UTC] sniper@php.net
Configure PHP with this configure line:

# rm config.cache ; ./configure --disable-all --disable-cgi --enable-debug
# make 

Then test, if it fails -> send your main/php_config.h to me.
 [2006-01-03 22:48 UTC] jgmtfia at gmail dot com
When I use the configure line:
./configure --disable-all --disable-cgi --enable-debug

The test passes and the strace output looks very much like that of php 4.3.10-15.

write(1, "Checking A.\n", 12) = 12
access("A", F_OK) = 0
write(1, "Checking C.\n", 12) = 12
access("C", F_OK) = 0
write(1, "Unlinking C.\n", 13) = 13
unlink("C") = 0
write(1, "Checking C: ", 12) = 12
access("C", F_OK) = -1 ENOENT (No such file or directory)

Which is the expected output.

I also tried ./configure with no arguments, which worked.  I then went back to my original configure was: 
 ./configure --with-apxs2=/usr/bin/apxs2 --enable-so \
 --with-xsl --with-xmlreader

I have narrowed it down to the 
 --with-apxs2=/usr/bin/apxs2 
flag.

When this ./configure flag is given the problem occurs in the cli version of PHP.  When this flag is not given the test passes.
 [2006-01-04 01:23 UTC] sniper@php.net
I'm still waiting for the php_config.h from the build the test fails with..
 [2006-01-04 18:53 UTC] jgmtfia at gmail dot com
php_config.h for both a working and non-working configuration were sent to sniper at php dot net.  The difference between the two was:

diff -puBb  ~/src/php_config.h-*
--- /home/user/src/php_config.h-broken        2006-01-04 09:30:07.000000000 -0700
+++ /home/user/src/php_config.h-working       2006-01-04 09:56:57.000000000 -0700
@@ -907,22 +907,22 @@
 /* #undef ROXEN_USE_ZTS */

 /* whether write(2) works */
-/* #undef PHP_WRITE_STDOUT */
+#define PHP_WRITE_STDOUT 1

 /*   */
-/* #undef FORCE_CGI_REDIRECT */
+#define FORCE_CGI_REDIRECT 0

 /*   */
-/* #undef DISCARD_PATH */
+#define DISCARD_PATH 0

 /*   */
-/* #undef ENABLE_PATHINFO_CHECK */
+#define ENABLE_PATHINFO_CHECK 1

 /*   */
-/* #undef PHP_FASTCGI */
+#define PHP_FASTCGI 0

 /*   */
-/* #undef PHP_FCGI_STATIC */
+#define PHP_FCGI_STATIC 0

 /* Define to the necessary symbol if this constant
                            uses a non-standard name on your system. */
@@ -2543,7 +2543,7 @@
 #define PHP_CAN_SUPPORT_PROC_OPEN 1

 /* Whether to enable chroot() function */
-/* #undef ENABLE_CHROOT_FUNC */
+#define ENABLE_CHROOT_FUNC 1

 /*   */
 #define HAVE_RES_NMKQUERY 1
@@ -2798,7 +2798,7 @@
 #define USE_ZEND_ALLOC 1

 /*   */
-#define ZTS 1
+/* #undef ZTS */

 /* Memory limit */
 #define MEMORY_LIMIT 0
@@ -2819,7 +2819,7 @@
 #define ZEND_MM_ALIGNMENT_LOG2 2

 /*   */
-#define ZTS 1
+/* #undef ZTS */

 /* Whether you use GNU Pth */
 /* #undef GNUPTH */
@@ -2831,7 +2831,7 @@
 /* #undef BETHREADS */

 /* Whether to use Pthreads */
-#define PTHREADS 1
+/* #undef PTHREADS */

 /* PHP build date */
 #define PHP_BUILD_DATE "2006-01-04"
@@ -2938,4 +2938,4 @@ int zend_sprintf(char *buffer, const cha
  * indent-tabs-mode: t
  * End:
  */
-#define PTHREADS 1
+/* #undef PTHREADS */
 [2006-01-04 20:44 UTC] sniper@php.net
Send also the config.log file.
 [2006-01-04 20:46 UTC] sniper@php.net
And before that: Switch to using non-threaded MPM in your apache. Threaded MPMs are not supported by PHP.
 [2006-01-04 23:44 UTC] jgmtfia at gmail dot com
I removed the apache2 threaded MPM that I had and replaced it with the prefork MPM and the test now passes when ./configure --with-apxs2=/usr/bin/apxs2 is used.

Thank you for your help with this.  I had no reason to suspect this was an apache related problem as the CLI API had the same problem as the apache2 module.

Is it possible to modify the configure script to detect when a threaded apache2 MPM is being used to avoid problems in the future?

On another note, the strace of the cli PHP running the test case now looks more like what would be expected:
write(1, "Checking A.\n", 12C) = 12
access("A", F_OK) = 0
write(1, "Checking C.\n", 12) = 12
access("C", F_OK) = 0
write(1, "Unlinking C.\n", 13) = 13
unlink("C") = 0
write(1, "Checking C: ", 12) = 12
access("C", F_OK) = -1 ENOENT (No such file or directory)

Again thank you.
 [2006-01-05 00:55 UTC] sniper@php.net
Can you still send me the config.log you got with the threaded server? I'd like to see why some of those tests failed..
 [2006-01-05 16:36 UTC] jgmtfia at gmail dot com
I will provide it to your email address this morning.
 [2006-01-05 20:10 UTC] jgmtfia at gmail dot com
The test given above passes when the non-threaded apache2 MPM is used for both the cli and apache2 API's.

However this test, which shows what I actually need to accomplish, fails on both the cli and apache2 API's.  Same sort of problem:
<?php

`echo file A > A; echo file B > B; ln -fs A C`;

echo 'Checking: ';

$FILES = array('A', 'B', 'C');
foreach(array('A', 'B', 'C') as $FILE){
    echo "$FILE ";
    if(!file_exists($FILE)){
	echo "$FILE does not exist.<br>";
	exit;
    }
}

echo "- ok<br><pre>".`ls -l A B C 2>&1`."</pre><br>";

clearstatcache();

echo "Contents of C => ".file_get_contents('C')."<br>";

echo "Remove C -> A, replace with C -> B<br>";
unlink('C');
clearstatcache();
symlink('B', 'C');

echo "<br><pre>".`ls -l A B C 2>&1`."</pre><br>";

$B = trim(file_get_contents('C'));
$RES = '<font color="'.(($B == 'file B') ?
    'green">Pass' : 'red">Fail').'</font>';

echo "Contents of C => $B $RES<br>";
?>

Gives the output:

Checking: A B C - ok

-rw-r--r--    1 nobody   -1           7 Jan  5 19:05 A
-rw-r--r--    1 nobody   -1           7 Jan  5 19:05 B
lrwxrwxrwx    1 nobody   -1           1 Jan  5 19:05 C -> A


Contents of C => file A
Remove C -> A, replace with C -> B

-rw-r--r--    1 nobody   -1           7 Jan  5 19:05 A
-rw-r--r--    1 nobody   -1           7 Jan  5 19:05 B
lrwxrwxrwx    1 nobody   -1           1 Jan  5 19:05 C -> B


Contents of C => file A Fail

An strace of the cli version running the given test shows again that php is doing an 
    open("/home/user/x/A", O_RDONLY)
when it should be doing    
    open("/home/user/x/C", O_RDONLY)

So it is the same kind of problem as the first test shows, but it did not go away when the apache MPM was changed to non-threaded.
 [2006-01-05 21:41 UTC] sniper@php.net
Yes, but it's just that very very old Debian with very very old glibc. Wont fix.
 [2006-01-06 02:41 UTC] jgmtfia at gmail dot com
The last test also fails on an up to date debian testing system (with or without an apache2 module)
 
PHP Copyright © 2001-2017 The PHP Group
All rights reserved.
Last updated: Sun Nov 19 01:31:42 2017 UTC