From 38d6b2014899f9c2d06b50f798f6b30acefd9bee Mon Sep 17 00:00:00 2001 From: Aaron Schulz Date: Tue, 7 Feb 2012 00:35:55 +0000 Subject: [PATCH] r109469: protect FileOpScopedPHPTimeout from abuse by infinite loops. Also commented out the invocation of it for now. --- includes/filerepo/backend/FileOp.php | 41 ++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/includes/filerepo/backend/FileOp.php b/includes/filerepo/backend/FileOp.php index 5197100d4f..e51f5b1a27 100644 --- a/includes/filerepo/backend/FileOp.php +++ b/includes/filerepo/backend/FileOp.php @@ -118,7 +118,8 @@ abstract class FileOp { // Restart PHP's execution timer and set the timeout to safe amount. // This handles cases where the operations take a long time or where we are // already running low on time left. The old timeout is restored afterwards. - $scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC ); + # @TODO: re-enable this for when the number of batches is high. + #$scopedTimeLimit = new FileOpScopedPHPTimeout( self::TIME_LIMIT_SEC ); // Attempt each operation... foreach ( $performOps as $index => $fileOp ) { @@ -359,28 +360,52 @@ abstract class FileOp { * the original time limit minus the time the object existed. */ class FileOpScopedPHPTimeout { - protected $startTime; // integer seconds - protected $oldTimeout; // integer seconds + protected $startTime; // float; seconds + protected $oldTimeout; // integer; seconds + + protected static $stackDepth = 0; // integer + protected static $totalCalls = 0; // integer + protected static $totalElapsed = 0; // float; seconds + + /* Prevent callers in infinite loops from running forever */ + const MAX_TOTAL_CALLS = 1000000; + const MAX_TOTAL_TIME = 300; // seconds /** * @param $seconds integer */ public function __construct( $seconds ) { if ( ini_get( 'max_execution_time' ) > 0 ) { // CLI uses 0 - $this->oldTimeout = ini_set( 'max_execution_time', $seconds ); + if ( self::$totalCalls >= self::MAX_TOTAL_CALLS ) { + trigger_error( "Maximum invocations of " . __CLASS__ . " exceeded." ); + } elseif ( self::$totalElapsed >= self::MAX_TOTAL_TIME ) { + trigger_error( "Time limit within invocations of " . __CLASS__ . " exceeded." ); + } elseif ( self::$stackDepth > 0 ) { // recursion guard + trigger_error( "Resursive invocation of " . __CLASS__ . " attempted." ); + } else { + $this->oldTimeout = ini_set( 'max_execution_time', $seconds ); + $this->startTime = microtime( true ); + ++self::$stackDepth; + ++self::$totalCalls; // proof against < 1us scopes + } } - $this->startTime = time(); } - /* + /** * Restore the original timeout. * This does not account for the timer value on __construct(). */ public function __destruct() { if ( $this->oldTimeout ) { - $elapsed = time() - $this->startTime; + $elapsed = microtime( true ) - $this->startTime; // Note: a limit of 0 is treated as "forever" - set_time_limit( max( 1, $this->oldTimeout - $elapsed ) ); + set_time_limit( max( 1, $this->oldTimeout - (int)$elapsed ) ); + // If each scoped timeout is for less than one second, we end up + // restoring the original timeout without any decrease in value. + // Thus web scripts in an infinite loop can run forever unless we + // take some measures to prevent this. Track total time and calls. + self::$totalElapsed += $elapsed; + --self::$stackDepth; } } } -- 2.20.1