Time Tested PHPAdvanced testing techniques with libTimeMachine Nick Galbreath firstname.lastname@example.org @ngalbreath Vince Tse email@example.com @vtonehundred 2012-07-19
Follow along or get the latest version at:http://slidesha.re/ NDc5mK
Time HappensWhile it should be avoided as much as possible, sometimes"time happens" and applications need testing based onsimulated time.• Financial applications (e.g. simulating ad spending and budgeting)• Security features (what happens when the cookie or auth token expires?)• System testing (what happens on leap year? day light savings time? 2038?)• Anything that runs periodically ("on the hour")
PHP Time Sources• $_SERVER[REQUEST_TIME]• time()• microtime()• gettimeofday()• Single argument of date(fmt) (equivalent to date(fmt, time())
Using $_SERVER[REQUEST_TIME]• Available in all SAPI contexts (mod_php, CLI, CGI, FPM...)• Created once at time of request• "Lowest Cost" -- array lookup• Easy to spoof in unit tests• Cant spoof for functional tests• Cant use it for timing
Passing as Argument• Dont call time() et al directly in a function but instead pass current time in.• Allows unit testing• Follows dependency injection best-practice
But what if your code isnt or cant bestructured that way?
Time Travel with libtimemachine!https://github.com/vtonehundred/libtimemachine Changes the system calls that PHP uses to get the current time • time (deﬁned in <time.h>) • gettimeofday (deﬁned in <sys/time.h>) • clock_gettime (deﬁned in <time.h>) and allows you to change them backwards or forward, relative or absolute.
LibTimeMachine• Use some secret loader sexiness to change the underlying system calls. (see man ld-linux for details)• Works on Linux systems• Works on Mac OS X (only tested on 10.7.4)• Sorry Windows• (not sure about FreeBSD)
Plug and Playgit clone git://github.com/vtonehundred/libtimemachine.gitcd libtimemachinemakesudo cp libtimemachine.so [ /lib64 or /lib ]sudo ldconfig
To use!• libtimemachine reads /tmp/libtimemachine.conf (or whatever ﬁle you want using the LIBTIMEMACHINE_CONF environment variable)• Single number controls how to adjust time• If starts with "-" or "+" then current time will be adjusted by a relative amount.• If "just numbers" then the time is ﬁxed with this value• If "0" or missing, then use current time
PHP CLIJust add LD_PRELOAD=libtimemachine.sobefore php on the command line$ php -r echo date("rn");Mon, 28 May 2012 23:03:38 -0400$ # go back one year$ echo "-31536000" > /tmp/libtimemachine.conf$ LD_PRELOAD=libtimemachine.so php -r echo date("rn");Sun, 29 May 2011 23:03:49 -0400$ #winning
PHP 5.4 Built-In WebServer This is the easiest way to go!$ dateMon May 28 23:27:19 2012$ echo "31536000" > /tmp/libtimemachine.conf$ LD_PRELOAD=/lib64/libtimemachine.so ./php -t ~/root -S 127.0.0.1:80PHP 5.4.3 Development Server started at Tue May 28 23:29:19 2013Listening on 127.0.0.1:80Document root is ~/rootPress Ctrl-C to quit.[Tue May 28 23:29:22 2013] 127.0.0.1:34913 : ~/time.php Command line CGI works similarly
Apache mod_php Debian / Ubuntu• Install libtimemachine.so in /lib64 or / lib depending on your OS.• (for good measure also do "sudo ldconfig")• /etc/apache2/envvars controls the apache and workers environment. Add export LD_PRELOAD=libtimemachine.so• sudo /etc/init.d/apache2 restart
Back One Day!$ dateSun, 27 May 2012 19:35:41 +0000$ echo "-86400" > /tmp/libtimemachine.conf$ curl http://127.0.0.1/phptime.phpREQUEST_TIME : Sat, 26 May 2012 19:35:54 +0000time() : Sat, 26 May 2012 19:35:54 +0000microtime() : Sat, 26 May 2012 19:35:54 +0000date(r) : Sat, 26 May 2012 19:35:54 +0000gettimeofday() : Sat, 26 May 2012 19:35:54 +0000
apache mod_php RedHat/CentOS• Disable SELinux: in /etc/selinux/config set SELINUX=disabled• put libtimemachine.so in /lib64 or /lib depending on your OS.• (for good measure also do "sudo ldconfig")• add to /etc/sysconfig/httpd export LD_PRELOAD=libtimemachine.so• And then...
Fail on Apache +mod_php + CentOS 6.2• SELinux removes LD_PRELOAD• Even though we disabled SELinux, it appears the linker isnt getting LD_PRELOAD• mod_php is an shared library that loads shared libraries. hmmm• I suspect a bug in the OS? Or maybe mod_php is compiled differently.• Use PHP 5.4s built-in web server instead for testing.
Future Work• Apache + PHP CGI (does anyone do this?)• nginx + PHP FPM (the new hotness)• Figuring out what is going on with CentOS• Testing on mysql server.• Packaging
Detecting libtimemachine• Look for existence of /tmp/libtimemachine.conf• Shell out and use "date +%s" and compare to time()• Use Apache mod_env and add PassEnv LD_PRELOAD to let PHP see the environment variable
Evil• Can this technique be used for evil?• Oh yeah.• type "LD_PRELOAD rootkit" in your favorite search engine for details
Mac OS X Notes• Only tested on 10.7.4• Mac OS X uses dyld for linking and works different than gnu ld. See man dyld for details.• Instead of LD_PRELOAD, use: DYLD_INSERT_LIBRARIES=./libtimemachine.dylib• If that doesnt work, add DYLD_FORCE_FLAT_NAMESPACE=1
Gotchas• if you globally set LD_PRELOAD, export LD_PRELOAD=libtimemachine.so then everything you do might be time shifted (to undo unset LD_PRELOAD)• Your application might run a bit slower since every time lookup requires reading /tmp/libtimemachine.conf
Thanks!https://github.com/vtonehundred/libtimemachine Nick Galbreath firstname.lastname@example.org @ngalbreath Vince Tse email@example.com @vtonehundred