Edit report at https://bugs.php.net/bug.php?id=52312&edit=1
ID: 52312
Comment by: pembo13 at gmail dot com
Reported by: v dot damore at gmail dot com
Summary: PHP safe_mode/open_basedir - lstat performance
problem
Status: Analyzed
Type: Bug
Package: Safe Mode/open_basedir
Operating System: Linux
PHP Version: 5.2.13
Block user comment: N
Private report: N
New Comment:
I seem to be suffering from this bug on an Apache/Linux + nfs setup. I'm using open_basedir and
the performance is so poor, that I can't even really stress test the server any more.
Previous Comments:
------------------------------------------------------------------------
[2013-05-29 21:45:36] phpdotnet at hostultra dot com
This bug is a real performance killer.
I propose this solution...
1. Modify symlink() php function so that if open_basedir or safe_mode is on, it disallows relative
symlinks with .. components, instead it creates absolute symlink.
This prevents attacker from exploiting CVE-2006-5178.
2. Allow the realpath cache to work even if open_basedir or safe_mode is on.
I did this with my own php code.
Before
---------
last pid: 30437; load averages: 32.93, 24.67, 15.62
98 processes: 43 running, 46 sleeping, 9 lock
CPU: 2.7% user, 0.0% nice, 75.3% system, 0.5% interrupt, 21.5% idle
After
---------
last pid: 30582; load averages: 2.06, 3.57, 10.58
68 processes: 6 running, 62 sleeping
CPU: 6.8% user, 0.0% nice, 1.7% system, 0.9% interrupt, 90.6% idle
------------------------------------------------------------------------
[2013-03-05 13:11:32] Terry at ellisons dot org dot uk
Rasmus, picking up our 2013-02-22 23:17 / 23:26 UTC conversation, I've thought about this some
further and gone through some test cases on the debugger.
Having walked through these call stacks, my view is that the PHP / Zend path scanning, file checking
and opening is a tangled mess. A typical open goes through sometimes 7 seven wrapping layers each
of which can do compound path resolution. The only reason that this doesn't end up slugging
performance is because the lowest level tsrm_realparth_r() uses a resolution cache and
short-circuits the actual I/O requests 95+% of the time.
The CVE-2006-5178 advisory really to a vulerability in the open_basedir checks. The root issue was
that a key check in php_check_specific_open_basedir() was using this cached path resolution; this
introduced the vulnerability because the caching enabled a race condition. The fix was to turn of
ALL realpath resolution caching. Yes this addressed the vulnerability, but at the price of killing
performance in the typical usecase where open_basedir might be used. This was unnecessary overkill.
If you think about it realpath caching itself doesn't introduce any vulnerability. The issue
is that the open itself -- or in this case the preceding php_check_specific_open_basedir() check
must disable any caching for that one check alone. Consider an example there the base dir is /a/b/c
and some path needs to be checked by php_check_specific_open_basedir(). There's not
vulnerability introduced by any previous resolution being used. This will result in one of two
scenarios:
* The resolved path is not of the form /a/b/c.... in which case the error is thrown
* The resolved path is of the form /a/b/c... but the actual path might contain raced symlinks, so
it must be scanned (by tsrm_realparth_r) to ensure that no links are extant immediately prior to the
open. There is also a sound case for curtailing this scan on a root-owned directory, but this of
second order.
Implementing this is a small code change.
(i) First drop the open_basedir predicated clearing of CWDG(realpath_cache_size_limit) = 0 in
main/main.c:php_module_startup().
(ii) introduce a per-call mechanism for disabling the cache. This could be done by adding a flag
to realpath, but the two variants (ZTS and none-ZTS) and the wide use of the wrapper VCWD_REALPATH()
macro might complicate this. An alternative would be to add another flag to the PG() structure,
checking this in tsrm_realparth_r() and setting it in php_check_specific_open_basedir() around the
VCWD_REALPATH(path_tmp, resolved_name) call.
Reactions / Thoughts? Is it worth me proposing a patch?
------------------------------------------------------------------------
[2013-02-23 16:06:50] Terry at ellisons dot org dot uk
Yes Rasmus. We both know that; but this won't be address without something like an LPC-style
file-based cache to preserve context across image activations, but all this isn't that relevant
to #52312 -- "PHP safe_mode/open_basedir - lstat performance problem".
What is relevant are my points about a require_once 6 sub-directories down taking 13 stats and 1
open with open_basedir unset and 60 stats and 1 open if it is set, and that the security requirement
could still be implemented within the former stat number if done correctly.
This isn't a material problem for single source file scripts, but MediaWiki, Wordpress and the
like typically load in ~100 modules generating ~6K stats per request. And this does become a
problem.
------------------------------------------------------------------------
[2013-02-23 15:22:59] rasmus@php.net
First-request cli isn't going to have a populated realpath cache no matter what
we do since this cache is per-process and in no way shared nor persistent across
different processes.
------------------------------------------------------------------------
[2013-02-23 13:20:03] Terry at ellisons dot org dot uk
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.
------------------------------------------------------------------------
The remainder of the comments for this report are too long. To view
the rest of the comments, please view the bug report online at
https://bugs.php.net/bug.php?id=52312
--
Edit this bug report at https://bugs.php.net/bug.php?id=52312&edit=1