PHP and Solaris: getcwd() Behavior November 10, 2007

The Solaris OS has implemented, for quite some time, a permission restriction with respect to when getcwd() will return a full/real path under certain conditions. This has at least been since SunOS 5.8 and continuing on into 5.10. Many functions within the PHP codebase relied upon a universally working getcwd() [C] call to expand paths and to find out where a script is being executed. In particular, Solaris does not assume that getcwd() is a privilege that should be granted to users in directories that don’t have ‘r’ (read) permission, even if it has ‘x’ (execute) permissions. For example, in Solaris, getcwd() will fail if a directory anywhere underneath your current path has ‘–x’.

pwd vs. getcwd()

The ‘pwd’ command under most shells cannot be used to reliably test for this behavior since it caches and keeps track of ‘cd’ operations. ‘pwd’ will let you see the name of your current location in Solaris, but it does not do this via a getcwd(). Instead, it is simply spitting back a path that it recorded and cached from when you have issued a ‘cd /directory’. Therefore, it is necessary to explicitly call getcwd() to test this behavior out. The following code can be used to test the operation of getcwd() in a directory:

#include <stdio.h>
main()
{
        char str[200];
        if (getcwd(str, sizeof(str)) == 0)
                printf("getcwd failed!!!\n");
        else
                printf("CWD = %s\n",str);
}

Under Linux, getcwd() behaves normally with the restrictive permissions:

# uname -a
Linux x.y.z 2.6.9-42.ELsmp #1 SMP Wed Jul 12 23:27:17 EDT
2006 i686 athlon i386 GNU/Linux
# find ./localfs -ls
 32001    8 d--x--x--x   3 nobody   nobody       4096 Oct  1 13:05  ./localfs
 32002    8 d--x--x--x   2 nobody   nobody       4096 Oct  1 13:12  ./localfs/test123
 32004   12 -rwsr-xr-x   1 nobody   nobody       4878 Oct  1 13:06  ./localfs/test123/cwdtest
# su nobody
# cd /localfs/test123
# ./cwdtest
CWD = /localfs/test123
# pwd
/localfs/test123
#

Under Solaris, getcwd() does not work with the –x restrictive permissions:

# uname -a
SunOS opteron 5.10 Generic_118855-14 i86pc i386 i86pc
# find ./localfs -ls
449384    1 d--x--x--x   3 nobody   nobody        512 Oct  1 13:13  ./localfs
449386    1 d--x--x--x   2 nobody   nobody        512 Oct  1 13:13  ./localfs/test123
449388    7 -rwsr-xr-x   1 nobody   nobody       6552 Oct  1 12:57  ./localfs/test123/cwdtest
# su nobody
# cd /localfs/test123
# ./cwdtest
getcwd failed!!!
# pwd
/localfs/test123
#

Bug or “security” feature?

There are some who feel that this is a bug, not a feature. I tend to agree with it being termed a bug, especially since I cannot find a single other OS that implements this “feature”. Either way, it broke PHP under situations where site owners were attempting to implement tight security on websites with many different users who have their own directory for PHP content.

Changes to PHP 5.2.5 that deal with this

Prior to PHP 5.2.5, PHP would would fail for relative includes in these situations: [include("../dir/")]. Since PHP 5.2.5, we added proper handling for this Solaris quirk to the code base so that relative paths in include()/require() still work under these conditions. In addition, a patch was added to safe_mode that used to cause similar behavior when the getcwd() call failed.

[1245][root@opteron:/test/testa/test]$ ls -al
total 56150
d--x--x--x   2 root     root         512 Nov 11 12:45 .
d--x--x--x   4 root     root         512 Nov 11 12:42 ..
-rwxr-xr-x   1 root     root     16719040 Nov 11 12:44 php-5.2.0
-rwxr-xr-x   1 root     root     12007004 Nov 11 12:45 php-5.2.5
-rw-r--r--   1 root     root          35 Nov 11 12:43 test.php
[1245][root@opteron:/test/testa/test]$ cat ./test.php
<?php
include('../b/file.php');
?>
[1245][root@opteron:/test/testa/test]$ cat ../b/file.php
<?php
echo "I am a file in directory b.\n";
?>
[1245][root@opteron:/test/testa/test]$ su rob
[1246][rob@opteron:/test/testa/test]$ uname -a
SunOS opteron 5.10 Generic_118855-14 i86pc i386 i86pc
[1246][rob@opteron:/test/testa/test]$ ./php-5.2.0 ./test.php
 
Warning: include(../b/file.php): failed to open stream: No such file or directory in /test.php on line 2
 
Warning: include(): Failed opening '../b/file.php' for inclusion (include_path='.:/usr/local/lib/php') in /test.php on line 2
[1246][rob@opteron:/test/testa/test]$ ./php-5.2.5 ./test.php
I am a file in directory b.
[1246][rob@opteron:/test/testa/test]$

NFS Mounts can hide the problem

The issue can initially be perplexing to pin down. I originally ran into this problem on a machine with an NFS mounted directory like: /a/b/NFS/webstuff/, where ‘NFS’ was an external FS. Looking at the directory path, all permissions looked ok (r-x), so I at first dismissed it as not being the culprit of the broken include() functionality. Upon looking into the problem, it was found that the local directory called ‘NFS’, before the mount happens had the restrictive permissions that triggered the quirky Solaris behavior (–x). This was not immediately evident since after the mount happens, the /NFS path is (to the eye) replaced with the inode of the remotely mounted directory, which was (r-x). None-the-less, the OS must walk this directory tree, including the /NFS directory inode on the local FS to get to the remotely mounted content.

  Directory: /a/b/NFS/webstuff
 
  inode(a) --> inode(b) --> inode(NFS) --> inode(NFS) --> inode(webstuff)
  [local]      [local]      [local]        [remote NFS]   [remote NFS]  
 
                               ^
                               ^
                            hidden after mount

Upgrade your PHP!

So, long story short, I strongly encourage anyone using Solaris to upgrade to PHP 5.2.5 as you are likely to run into this situation if you have multiple users, NFS mounted web dirs, safe_mode=on, or another reason to implement strict directory permission in your installation.

Leave a Reply