|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2010-07-12 10:03 UTC] v dot damore at gmail dot com
Description:
------------
PHP lstat full pathname many times (at least 4) before read the file is looking for.
This behavior appear when in apache httpd configuration is specified PHP_ADMIN_VALUE open_basedir or safe_mode is On.
Test script:
---------------
To reproduce the problem please create a page phpinfo.php: "<? phpinfo() ?>".
I have httpd.2.2.15, PHP 5.2.13.
[root@svilpar4 ~]# /usr/local/apache2/bin/httpd -V
Server version: Apache/2.2.15 (Unix)
Server built: Jul 9 2010 17:30:06
Server's Module Magic Number: 20051115:24
Server loaded: APR 1.2.7, APR-Util 1.2.7
Compiled using: APR 1.2.7, APR-Util 1.2.7
Architecture: 64-bit
Server MPM: Prefork
threaded: no
forked: yes (variable process count)
Server compiled with....
-D APACHE_MPM_DIR="server/mpm/prefork"
-D APR_HAS_SENDFILE
-D APR_HAS_MMAP
-D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)
-D APR_USE_SYSVSEM_SERIALIZE
-D APR_USE_PTHREAD_SERIALIZE
-D SINGLE_LISTEN_UNSERIALIZED_ACCEPT
-D APR_HAS_OTHER_CHILD
-D AP_HAVE_RELIABLE_PIPED_LOGS
-D DYNAMIC_MODULE_LIMIT=128
-D HTTPD_ROOT="/usr/local/apache2"
-D SUEXEC_BIN="/usr/local/apache2/bin/suexec"
-D DEFAULT_PIDLOG="logs/httpd.pid"
-D DEFAULT_SCOREBOARD="logs/apache_runtime_status"
-D DEFAULT_LOCKFILE="logs/accept.lock"
-D DEFAULT_ERRORLOG="logs/error_log"
-D AP_TYPES_CONFIG_FILE="conf/mime.types"
-D SERVER_CONFIG_FILE="conf/httpd.conf"
[root@svilpar4 ~]# /usr/local/php5.2.13/bin/php -v
PHP 5.2.13 (cli) (built: Jul 1 2010 16:02:03)
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2010 Zend Technologies
Now we specify PHP_ADMIN_VALUE open_basedir</strong> in Virtual host configuration:
<Directory "/usr/local/myspace/webspace/httpdocs">
PHP_ADMIN_VALUE open_basedir "/usr/local/myspace/webspace"
</Directory>
<VirtualHost *:80>
ServerName damorealt.xoom.it
DocumentRoot "/usr/local/myspace/webspace/httpdocs"
CustomLog /var/log/httpd/damorealt/access_log combined
ErrorLog /var/log/httpd/damorealt/error_log
</VirtualHost >
Stop & start apache httpd, "strace -f" all httpd instances and then call page http://damorealt.xoom.it/phpinfo.php, so we can reproduce behavior
Expected result:
----------------
If PHP_ADMIN_VALUE open_basedir "/usr/local/myspace/webspace" is removed and safe_mode is Off :
226235 accept(3, {sa_family=AF_INET, sin_port=htons(59366), sin_addr=inet_addr("212.48.14.186")}, [17179869200]) = 15
26235 getsockname(15, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("151.99.197.198")}, [17179869200]) = 0
26235 fcntl(15, F_GETFL) = 0x2 (flags O_RDWR)
26235 fcntl(15, F_SETFL, O_RDWR|O_NONBLOCK) = 0
26235 read(15, "GET /phpinfo.php HTTP/1.0\r\nUser-"..., 8000) = 129
26235 gettimeofday({1278696735, 988799}, NULL) = 0
26235 stat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
26235 open("/usr/local/myspace/.htaccess", O_RDONLY) = -1 ENOENT (No such file or directory)
26235 open("/usr/local/myspace/webspace/.htaccess", O_RDONLY) = -1 ENOENT (No such file or directory)
26235 open("/usr/local/myspace/webspace/httpdocs/.htaccess", O_RDONLY) = -1 ENOENT (No such file or directory)
26235 open("/usr/local/myspace/webspace/httpdocs/phpinfo.php/.htaccess", O_RDONLY) = -1 ENOTDIR (Not a directory)
26235 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={20, 0}}, NULL) = 0
26235 rt_sigaction(SIGPROF, {0x2afef587dd80, [PROF], SA_RESTORER|SA_RESTART, 0x3916e302d0}, {SIG_DFL, [], 0}, 8) = 0
26235 rt_sigprocmask(SIG_UNBLOCK, [PROF], NULL, 8) = 0
26235 getcwd("/"..., 4095) = 2
26235 chdir("/usr/local/myspace/webspace/httpdocs") = 0
water boiling point
26235 time(NULL) = 1278696735
26235 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
26235 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
26235 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
26235 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
26235 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
26235 lstat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
And read the file.
26235 open("/usr/local/myspace/webspace/httpdocs/phpinfo.php", O_RDONLY) = 16
26235 fstat(16, {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
26235 read(16, "<? phpinfo() ?>\n", 8192) = 16
26235 read(16, "", 8192) = 0
26235 read(16, "", 8192) = 0
26235 close(16) = 0
26235 uname({sys="Linux", node="svilpar4", ...}) = 0
26235 time(NULL) = 1278696735
26235 writev(15, [{"HTTP/1.1 200 OK\r\nDate: Fri, 09 J"..., 173}, {"<!DOCTYPE html PUBLIC \"-//W3C//D"..., 4109}, {"<table border=\"0\" cellpadding=\"3"..., 4101}], 3) = 8383
26235 writev(15, [{"<tr><td class=\"e\">highlight.bg</"..., 4105}, {"sendmail_from</td><td class=\"v\">"..., 4099}], 2) = 8204
26235 time(NULL) = 1278696735
26235 writev(15, [{" </td></tr>\n<tr><td class=\"e\">HT"..., 4108}, {"</td><td class=\"v\">1024</td><td "..., 4098}], 2) = 8206
26235 writev(15, [{"md2 md4 md5 sha1 sha256 sha384 s"..., 4098}, {" </td></tr>\n</table><br />\n<tabl"..., 4106}], 2) = 8204
26235 writev(15, [{"session.use_cookies</td><td clas"..., 4104}, {" </td><td class=\"v\">enabled </td"..., 4102}], 2) = 8206
26235 chdir("/") = 0
26235 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={0, 0}}, NULL) = 0
26235 writev(15, [{"\"]</td><td class=\"v\">Keep-Alive<"..., 4206}], 1) = 4206
26235 write(10, "212.48.14.186 - - [09/Jul/2010:1"..., 116) = 116
26235 shutdown(15, 1 /* send */) = 0
26235 poll([{fd=15, events=POLLIN}], 1, 2000) = 1 ([{fd=15, revents=POLLIN|POLLHUP}])
26235 read(15, "", 512) = 0
26235 close(15) = 0
26235 read(4, 0x7fff615ff5eb, 1) = -1 EAGAIN (Resource temporarily unavailable)
26235 accept(3,
Actual result:
--------------
If PHP_ADMIN_VALUE open_basedir "/usr/local/myspace/webspace" is set and safe_mode is On :
25933 accept(3, {sa_family=AF_INET, sin_port=htons(47433), sin_addr=inet_addr("212.48.14.186")}, [17179869200]) = 15
25933 getsockname(15, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("151.99.197.198")}, [17179869200]) = 0
25933 fcntl(15, F_GETFL) = 0x2 (flags O_RDWR)
25933 fcntl(15, F_SETFL, O_RDWR|O_NONBLOCK) = 0
25933 read(15, "GET /phpinfo.php HTTP/1.0\r\nUser-"..., 8000) = 129
25933 gettimeofday({1278695388, 52976}, NULL) = 0
25933 stat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
25933 open("/usr/local/myspace/.htaccess", O_RDONLY) = -1 ENOENT (No such file or directory)
25933 open("/usr/local/myspace/webspace/.htaccess", O_RDONLY) = -1 ENOENT (No such file or directory)
25933 open("/usr/local/myspace/webspace/httpdocs/.htaccess", O_RDONLY) = -1 ENOENT (No such file or directory)
25933 open("/usr/local/myspace/webspace/httpdocs/phpinfo.php/.htaccess", O_RDONLY) = -1 ENOTDIR (Not a directory)
25933 setitimer(ITIMER_PROF, {it_interval={0, 0}, it_value={20, 0}}, NULL) = 0
25933 rt_sigaction(SIGPROF, {0x2b80442fdd80, [PROF], SA_RESTORER|SA_RESTART, 0x3916e302d0}, {SIG_DFL, [], 0}, 8) = 0
25933 rt_sigprocmask(SIG_UNBLOCK, [PROF], NULL, 8) = 0
25933 getcwd("/"..., 4095) = 2
25933 chdir("/usr/local/myspace/webspace/httpdocs") = 0
water boiling point
25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
First check
25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
Second check
25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
Third check (incomplete)
25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
25933 lstat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
Final check and then read the file.
25933 open("/usr/local/myspace/webspace/httpdocs/phpinfo.php", O_RDONLY) = 16
25933 fstat(16, {st_mode=S_IFREG|0644, st_size=16, ...}) = 0
25933 read(16, "<? phpinfo() ?>\n", 8192) = 16
25933 read(16, "", 8192) = 0
25933 read(16, "", 8192) = 0
25933 close(16) = 0
PatchesPull Requests
Pull requests:
HistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sat Oct 25 22:00:01 2025 UTC |
The real problem is that such full scan are repeated more then once: 25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0 First check 25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0 Second check 25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 Third check (incomplete) 25933 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 25933 lstat("/usr/local/myspace/webspace/httpdocs/phpinfo.php", {st_mode=S_IFREG|0644, st_size=16, ...}) = 0 Final check and then read the file. 25933 open("/usr/local/myspace/webspace/httpdocs/phpinfo.php", O_RDONLY) = 16 25933 fstat(16, {st_mode=S_IFREG|0644, st_size=16, ...}) = 0 25933 read(16, "<? phpinfo() ?>\n", 8192) = 16 25933 read(16, "", 8192) = 0 25933 read(16, "", 8192) = 0 25933 close(16) = 0I have already tried with 5.3 and I was thinking to open an new bug because I have same behavior, there is only a change in the order of execution of lstat from fullpath to /usr : 7339 chdir("/usr/local/myspace/webspace/httpdocs") = 0 7339 lstat("/usr/local/myspace/webspace/httpdocs/test.php", {st_mode=S_IFREG|0644, st_size=19, ...}) = 0 7339 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace/webspace/httpdocs/test.php", {st_mode=S_IFREG|0644, st_size=19, ...}) = 0 7339 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace/webspace/httpdocs/test.php", {st_mode=S_IFREG|0644, st_size=19, ...}) = 0 7339 lstat("/usr/local/myspace/webspace/httpdocs", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace/webspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local/myspace", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr/local", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 lstat("/usr", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 7339 open("/usr/local/myspace/webspace/httpdocs/test.php", O_RDONLY) = 16 7339 fstat(16, {st_mode=S_IFREG|0644, st_size=19, ...}) = 0 7339 fstat(16, {st_mode=S_IFREG|0644, st_size=19, ...}) = 0 7339 fstat(16, {st_mode=S_IFREG|0644, st_size=19, ...}) = 0 7339 mmap(NULL, 19, PROT_READ, MAP_SHARED, 16, 0) = 0x2adfec195000 7339 munmap(0x2adfec195000, 19) = 0 7339 close(16) = 0 7339 chdir("/") = 0After having set breakpoint tsrm_realpath_r and I have execute step by step debug. I think is interesting that after execution tsrm_virtual_cwd.c of line 681 execution continue on line 890. gdb) break tsrm_realpath_r Breakpoint 1 at 0x2b0b3c9f2702: file /usr/local/sitipersonali/sitipersonali01/NSP_SERVICE/strillo/sources/php-5.3.2/TSRM/tsrm_virtual_cwd.c, line 611. (gdb) continue Continuing. Breakpoint 1, tsrm_realpath_r (path=0x7fffddfb32b0 "/usr/local/myspace/webspace/httpdocs/test.php", start=1, len=45, ll=0x7fffddfb32ac, t=0x7fffddfb32a0, use_realpath=2, is_dir=0, link_is_dir=0x0) at /usr/local/sitipersonali/sitipersonali01/NSP_SERVICE/strillo/sources/php-5.3.2/TSRM/tsrm_virtual_cwd.c:611 611 int directory = 0; (gdb) step 624 if (len <= start) { (gdb) step 628 i = len; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 630 i--; (gdb) step 629 while (i > start && !IS_SLASH(path[i-1])) { (gdb) step 633 if (i == len || (gdb) step 639 } else if (i == len - 2 && path[i] == '.' && path[i+1] == '.') { (gdb) step 677 path[len] = 0; (gdb) step 679 save = (use_realpath != CWD_EXPAND); (gdb) step 681 if (start && save && CWDG(realpath_cache_size_limit)) { (gdb) watch save Hardware watchpoint 2: save (gdb) print save $1 = 1 (gdb) print start $2 = 1 (gdb) print realpath_cache_size_limit No symbol "realpath_cache_size_limit" in current context. (gdb) step 890 if (save && lstat(path, &st) < 0) { (gdb)I found where the problem is, this behavior is not a bug. Looking at main/main.c I found following lines: 1416: /* Disable realpath cache if safe_mode or open_basedir are set */ if (PG(safe_mode) || (PG(open_basedir) && *PG(open_basedir))) { CWDG(realpath_cache_size_limit) = 0; } 1978: /* Disable realpath cache if safe_mode or open_basedir are set */ if (PG(safe_mode) || (PG(open_basedir) && *PG(open_basedir))) { CWDG(realpath_cache_size_limit) = 0; } Could you explain why if safe_mode or open_basedir are set realpath cache is disabled?Looking at source code main/main.c of 5.2.13 I can see: 1292: /* Disable realpath cache if safe_mode or open_basedir are set */ if (PG(safe_mode) || (PG(open_basedir) && *PG(open_basedir))) { CWDG(realpath_cache_size_limit) = 0; } 1769: /* Disable realpath cache if safe_mode or open_basedir are set */ if (PG(safe_mode) || (PG(open_basedir) && *PG(open_basedir))) { CWDG(realpath_cache_size_limit) = 0; } So realpath cache is definitely disabled in case of safe_mode or open_basedir. This dramatically reduce performance of PHP Engine and this behavior can bring a server to its knees. Especially because there is a lack of documentation! Can you explain why this choose? Must I continue debugging PHP engine in order to understand what's happening?Hello, I have recompiled php commenting both in main/main.c: /* if (PG(safe_mode) || (PG(open_basedir) && *PG(open_basedir))) { CWDG(realpath_cache_size_limit) = 0; } */ I have defined into TSRM/tsrm_virtual_cwd.c #define realpath(x,y) strcpy(y,x) and I have disabled PHP function symlink. Now I finally not have performance problems, but I suppose I still can have security problem. Can you help me in order to understand if there is a better solution?This bug is extant in 5.3 and 5.4 (up to 5.4.9). The flaw is the logic in main/main.c:php_module_startup(): /* Disable realpath cache if safe_mode or open_basedir are set */ if (PG(safe_mode) || (PG(open_basedir) && *PG(open_basedir))) { CWDG(realpath_cache_size_limit) = 0; } Similar logic validly exists in code such as suEXEC and suPHP to prevent a race condition allowing and unscrupulous script author to execute a script in another UID in certain circumstances. This simply doesn't apply in this case. Which technically a similar race could allow a script author to break the basedir contain with miniscule probability by exploiting such a race, the performance impacts are significant, for no material increase in security strength. Get rid of this rule or at least split open_basedir into two separate directives open_basedir_paranoid which disables the cache as above and open_basedir which does the rational checks and doesn't disable the cache. This has been lying around for nearly 3 years. How can we progress this?I don't know what your dd2.inc is doing, but I went through and optimized syscalls a couple of years ago, and in normal no-openbasedir mode things are pretty clean. Here are a few scenarios: a.php does a require on ./b.php and a require_once on ./c.php PHP 5.4/APC-HEAD/apc.stat=1/apc.include_once_override=0/open_basedir=off stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 stat("/var/www/./b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 open("/var/www/c.php", O_RDONLY) = 29 fstat(29, {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 fstat(29, {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 fstat(29, {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 stat("/var/www/c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 The initial stat on a.php is actually done by Apache and we ask Apache for the stat struct to avoid an extra stat there. You can see the effect of the open and then the (quick) fstat calls on fd=29. This is where APC tries to optimize things a bit using the include_once_override. If we turn that on we get: stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 stat("/var/www/./b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 stat("/var/www/./c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 stat("/var/www/./c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 Not ideal since there is still an extra stat for the require_once case, but the open() is gone. And you can eliminate those stats by turning off apc.stat which means only the initial top-page stat from Apache on a.php will be done. Now, of course, if we turn on open_basedir we get: www-data 1799 0.8 0.2 310232 22560 pts/6 S+ 19:26 0:00 /usr/sbin/apache2 -X root 1887 0.0 0.0 10892 916 pts/12 R+ 19:26 0:00 grep apache 7:26pm x220:~> strace -o out -p 1799 Process 1799 attached - interrupt to quit ^CProcess 1799 detached 7:26pm x220:~> egrep "stat|open" out stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 lstat("/var/www/./b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat("/var/www/b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 stat("/var/www/./b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www/./c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat("/var/www/c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 stat("/var/www/./c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www/./c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat("/var/www/c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 stat("/var/www/./c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 which is far from ideal, of course, because we are not hitting our realpath cache at all and there is an oddity there with ./ getting resolved and re-statted and this re- realpathed. Not sure I see where your 7 times thing is though. With ZO+ with this config zend_optimizerplus.revalidate_freq=0 zend_optimizerplus.enable_file_override=0 open_basedir= we see this: stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 stat("/var/www/b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 stat("/var/www/c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 Here we see that ZO+ doesn't have the Apache stat optimization that APC has, which is something we should obviously add, but it handles the require_once better and the open() call and fstats are gone. When we turn on open_basedir we get: stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 lstat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 lstat("/var/www/./b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 lstat("/var/www", {st_mode=S_IFDIR|0777, st_size=4096, ...}) = 0 lstat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 stat("/var/www/b.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 stat("/var/www/c.php", {st_mode=S_IFREG|0664, st_size=10, ...}) = 0 Better than APC, still not great, but there are no open() calls here, so the NFS getattr issue should be gone. And finally, ZO+ has a TTL version of apc.stat so we can set it to only stat every 60 seconds, so even with open_basedir enabled, with this config: zend_optimizerplus.revalidate_freq=60 zend_optimizerplus.enable_file_override=1 we get this: stat("/var/www/a.php", {st_mode=S_IFREG|0664, st_size=49, ...}) = 0 which is just the initial stat done by Apache which we can't do anything about. So, in general, assuming the 5.5 integrated opcode cache is based on ZO+ and we add the one missing Apache stat optimization, I really don't think the situation is all that dire, even for NFS-mounted scripts.The above discussion was largely about the I/O overheads with open_basedir specified, so my figures where in that context, and dd2.inc is just an empty class, but this isnt relevant to its inclusion. Try: echo "">dummy.inc pwd strace -tt -o /tmp/strace.log \ php -d realpath_cache_ttl=9999 -d open_basedir=$(pwd) \ -r '$x="./dummy.inc"; require_once($x);' to see what I mean (I did this from 6 dir levels down which is pretty typical of web set ups.) since you can do this yourself I won't dump the log output but: for vb in lstat fstat stat open; do echo -n "$vb " sed -ne '/dummy.inc/,/close(/p' /tmp/strace | grep -c " $vb" done lstat 54 fstat 5 stat 1 open 1 (*) I removed the open for "/etc/ld.so.cache" which isn't relevant here whereas dropping the open_basedir directive and repeating gives: lstat 8 fstat 5 stat 1 open 1 so the open_basedir can have a severe impact on performance. I am still at loss as to why the PHP cache is disabled if open_basedir is set. Surely the security objectives would be entirely achieved by doing the real walk only during the open routine itself? I realise that NFS tuning can mitigate these issues -- e.g. using noatime and setting the actimeo,... parameters or even using a local fscache (in Ubuntu this is the cachefilesd package). I agree that running mod_php5 with APC or O+ help a lot here, but as we've discussed in the past, APC and O+ do not support php-cgi or php-cli. (I know that APC has apc.enable_cli, but this seems to be a functional no-op.) We should have acceptable PHP performance over all common usecases.We have this issue with Symfony(3.1.3), it's really poor performance with lot of stat on file... With the same project on Windows (IIS 7.5 - PHP 5.6.1 With open_basedir (empty) it's take ~300-350ms for the index With open_basedir set (~9 path) it's take 3.5-10s for the index I understand security problem, but why not just clear cache (before and/or after risky function) things like that : -------------------- diff --git a/ext/standard/link.c b/ext/standard/link.c index 62e7295..a844f46 100644 --- a/ext/standard/link.c +++ b/ext/standard/link.c @@ -158,6 +158,11 @@ PHP_FUNCTION(symlink) RETURN_FALSE; } + /* Reset realpath_cache when open_basedir is not null to avoid security issues */ + if(PG(open_basedir)){ + realpath_cache_clean(); + } + /* For the source, an expanded path must be used (in ZTS an other thread could have changed the CWD). * For the target the exact string given by the user must be used, relative or not, existing or not. * The target is relative to the link itself, not to the CWD. */ @@ -206,6 +211,11 @@ PHP_FUNCTION(link) RETURN_FALSE; } + /* Reset realpath_cache when open_basedir is not null to avoid security issues */ + if(PG(open_basedir)){ + realpath_cache_clean(); + } + #ifndef ZTS ret = link(topath, frompath); #else diff --git a/ext/standard/link_win32.c b/ext/standard/link_win32.c index 7d43162..e47e265 100644 --- a/ext/standard/link_win32.c +++ b/ext/standard/link_win32.c @@ -168,6 +168,12 @@ PHP_FUNCTION(symlink) php_error_docref(NULL, E_WARNING, "UTF-16 conversion failed (error %d)", GetLastError()); RETURN_FALSE; } + + /* Reset realpath_cache when open_basedir is not null to avoid security issues */ + if(PG(open_basedir)){ + realpath_cache_clean(); + } + /* For the source, an expanded path must be used (in ZTS an other thread could have changed the CWD). * For the target the exact string given by the user must be used, relative or not, existing or not. * The target is relative to the link itself, not to the CWD. */ @@ -223,6 +229,11 @@ PHP_FUNCTION(link) RETURN_FALSE; } + /* Reset realpath_cache when open_basedir is not null to avoid security issues */ + if(PG(open_basedir)){ + realpath_cache_clean(); + } + #ifndef ZTS ret = CreateHardLinkA(topath, frompath, NULL); #else diff --git a/main/main.c b/main/main.c index bb98f27..7e5904e 100644 --- a/main/main.c +++ b/main/main.c @@ -2222,7 +2222,7 @@ int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_mod /* Disable realpath cache if an open_basedir is set */ if (PG(open_basedir) && *PG(open_basedir)) { - CWDG(realpath_cache_size_limit) = 0; + /* CWDG(realpath_cache_size_limit) = 0; */ } /* initialize stream wrappers registry --------------out-of-tree extensions are always a problem when it comes to minor nd major updates even even the stuff on pecl.php.net is to avoid whenever possible "Instead of setting open_basedir you will set realpath_turbo.open_basedir" is a crude hack which requires you to touch each and every vhost and that all because stubborn decisions for no gain ---------------------------------- [builduser@testserver:/rpmbuild/SOURCES]$ cat php-72-realpath-cache-openbasedir.patch --- php-7.1.9-original/main/main.c 2017-08-16 18:06:53.000000000 +0200 +++ php-7.1.9-patched/main/main.c 2017-08-17 14:11:05.827653357 +0200 @@ -1646,9 +1646,9 @@ } /* Disable realpath cache if an open_basedir is set */ - if (PG(open_basedir) && *PG(open_basedir)) { - CWDG(realpath_cache_size_limit) = 0; - } + /* if (PG(open_basedir) && *PG(open_basedir)) { */ + /* CWDG(realpath_cache_size_limit) = 0; */ + /*}*/ if (PG(expose_php)) { sapi_add_header(SAPI_PHP_VERSION_HEADER, sizeof(SAPI_PHP_VERSION_HEADER)-1, 1); @@ -2236,9 +2236,9 @@ #endif /* Disable realpath cache if an open_basedir is set */ - if (PG(open_basedir) && *PG(open_basedir)) { - CWDG(realpath_cache_size_limit) = 0; - } + /*if (PG(open_basedir) && *PG(open_basedir)) { */ + /* CWDG(realpath_cache_size_limit) = 0;*/ + /*}*/ /* initialize stream wrappers registry * (this uses configuration parameters from php.ini) ---------------------------------- [builduser@testserver:/rpmbuild/SOURCES]$ cat php-73-realpath-cache-openbasedir.patch --- php-7.3.0-original/main/main.c 2018-07-31 13:33:48.000000000 +0200 +++ php-7.3.0-patched/main/main.c 2018-07-31 14:01:15.144001078 +0200 @@ -1798,9 +1798,9 @@ } /* Disable realpath cache if an open_basedir is set */ - if (PG(open_basedir) && *PG(open_basedir)) { - CWDG(realpath_cache_size_limit) = 0; - } + /*if (PG(open_basedir) && *PG(open_basedir)) { */ + /* CWDG(realpath_cache_size_limit) = 0; */ + /*}*/ if (PG(expose_php)) { sapi_add_header(SAPI_PHP_VERSION_HEADER, sizeof(SAPI_PHP_VERSION_HEADER)-1, 1); @@ -2292,9 +2292,9 @@ #endif /* Disable realpath cache if an open_basedir is set */ - if (PG(open_basedir) && *PG(open_basedir)) { - CWDG(realpath_cache_size_limit) = 0; - } + /*if (PG(open_basedir) && *PG(open_basedir)) { */ + /* CWDG(realpath_cache_size_limit) = 0; */ + /*}*/ PG(have_called_openlog) = 0;