Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/poolcounter/PoolCounterRedis.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 * @author Aaron Schulz
20
 */
21
use Psr\Log\LoggerInterface;
22
23
/**
24
 * Version of PoolCounter that uses Redis
25
 *
26
 * There are four main redis keys used to track each pool counter key:
27
 *   - poolcounter:l-slots-*     : A list of available slot IDs for a pool.
28
 *   - poolcounter:z-renewtime-* : A sorted set of (slot ID, UNIX timestamp as score)
29
 *                                 used for tracking the next time a slot should be
30
 *                                 released. This is -1 when a slot is created, and is
31
 *                                 set when released (expired), locked, and unlocked.
32
 *   - poolcounter:z-wait-*      : A sorted set of (slot ID, UNIX timestamp as score)
33
 *                                 used for tracking waiting processes (and wait time).
34
 *   - poolcounter:l-wakeup-*    : A list pushed to for the sake of waking up processes
35
 *                                 when a any process in the pool finishes (lasts for 1ms).
36
 * For a given pool key, all the redis keys start off non-existing and are deleted if not
37
 * used for a while to prevent garbage from building up on the server. They are atomically
38
 * re-initialized as needed. The "z-renewtime" key is used for detecting sessions which got
39
 * slots but then disappeared. Stale entries from there have their timestamp updated and the
40
 * corresponding slots freed up. The "z-wait" key is used for detecting processes registered
41
 * as waiting but that disappeared. Stale entries from there are deleted and the corresponding
42
 * slots are freed up. The worker count is included in all the redis key names as it does not
43
 * vary within each $wgPoolCounterConf type and doing so handles configuration changes.
44
 *
45
 * This class requires Redis 2.6 as it makes use Lua scripts for fast atomic operations.
46
 * Also this should be on a server plenty of RAM for the working set to avoid evictions.
47
 * Evictions could temporarily allow wait queues to double in size or temporarily cause
48
 * pools to appear as full when they are not. Using volatile-ttl and bumping memory-samples
49
 * in redis.conf can be helpful otherwise.
50
 *
51
 * @ingroup Redis
52
 * @since 1.23
53
 */
54
class PoolCounterRedis extends PoolCounter {
55
	/** @var HashRing */
56
	protected $ring;
57
	/** @var RedisConnectionPool */
58
	protected $pool;
59
	/** @var LoggerInterface */
60
	protected $logger;
61
	/** @var array (server label => host) map */
62
	protected $serversByLabel;
63
	/** @var string SHA-1 of the key */
64
	protected $keySha1;
65
	/** @var int TTL for locks to expire (work should finish in this time) */
66
	protected $lockTTL;
67
68
	/** @var RedisConnRef */
69
	protected $conn;
70
	/** @var string Pool slot value */
71
	protected $slot;
72
	/** @var int AWAKE_* constant */
73
	protected $onRelease;
74
	/** @var string Unique string to identify this process */
75
	protected $session;
76
	/** @var int UNIX timestamp */
77
	protected $slotTime;
78
79
	const AWAKE_ONE = 1; // wake-up if when a slot can be taken from an existing process
80
	const AWAKE_ALL = 2; // wake-up if an existing process finishes and wake up such others
81
82
	/** @var PoolCounterRedis[] List of active PoolCounterRedis objects in this script */
83
	protected static $active = null;
84
85
	function __construct( $conf, $type, $key ) {
86
		parent::__construct( $conf, $type, $key );
87
88
		$this->serversByLabel = $conf['servers'];
89
		$this->ring = new HashRing( array_fill_keys( array_keys( $conf['servers'] ), 100 ) );
90
91
		$conf['redisConfig']['serializer'] = 'none'; // for use with Lua
92
		$this->pool = RedisConnectionPool::singleton( $conf['redisConfig'] );
93
		$this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
94
95
		$this->keySha1 = sha1( $this->key );
96
		$met = ini_get( 'max_execution_time' ); // usually 0 in CLI mode
97
		$this->lockTTL = $met ? 2 * $met : 3600;
0 ignored issues
show
Documentation Bug introduced by
It seems like $met ? 2 * $met : 3600 can also be of type double. However, the property $lockTTL is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
98
99
		if ( self::$active === null ) {
100
			self::$active = [];
101
			register_shutdown_function( [ __CLASS__, 'releaseAll' ] );
102
		}
103
	}
104
105
	/**
106
	 * @return Status Uses RediConnRef as value on success
107
	 */
108
	protected function getConnection() {
109
		if ( !isset( $this->conn ) ) {
110
			$conn = false;
111
			$servers = $this->ring->getLocations( $this->key, 3 );
112
			ArrayUtils::consistentHashSort( $servers, $this->key );
113
			foreach ( $servers as $server ) {
114
				$conn = $this->pool->getConnection( $this->serversByLabel[$server], $this->logger );
115
				if ( $conn ) {
116
					break;
117
				}
118
			}
119
			if ( !$conn ) {
120
				return Status::newFatal( 'pool-servererror', implode( ', ', $servers ) );
121
			}
122
			$this->conn = $conn;
123
		}
124
		return Status::newGood( $this->conn );
125
	}
126
127
	function acquireForMe() {
128
		$status = $this->precheckAcquire();
129
		if ( !$status->isGood() ) {
130
			return $status;
131
		}
132
133
		return $this->waitForSlotOrNotif( self::AWAKE_ONE );
134
	}
135
136
	function acquireForAnyone() {
137
		$status = $this->precheckAcquire();
138
		if ( !$status->isGood() ) {
139
			return $status;
140
		}
141
142
		return $this->waitForSlotOrNotif( self::AWAKE_ALL );
143
	}
144
145
	function release() {
146
		if ( $this->slot === null ) {
147
			return Status::newGood( PoolCounter::NOT_LOCKED ); // not locked
148
		}
149
150
		$status = $this->getConnection();
151
		if ( !$status->isOK() ) {
152
			return $status;
153
		}
154
		$conn = $status->value;
155
156
		// @codingStandardsIgnoreStart Generic.Files.LineLength
157
		static $script =
158
		/** @lang Lua */
159
<<<LUA
160
		local kSlots,kSlotsNextRelease,kWakeup,kWaiting = unpack(KEYS)
161
		local rMaxWorkers,rExpiry,rSlot,rSlotTime,rAwakeAll,rTime = unpack(ARGV)
162
		-- Add the slots back to the list (if rSlot is "w" then it is not a slot).
163
		-- Treat the list as expired if the "next release" time sorted-set is missing.
164
		if rSlot ~= 'w' and redis.call('exists',kSlotsNextRelease) == 1 then
165
			if 1*redis.call('zScore',kSlotsNextRelease,rSlot) ~= (rSlotTime + rExpiry) then
166
				-- Slot lock expired and was released already
167
			elseif redis.call('lLen',kSlots) >= 1*rMaxWorkers then
168
				-- Slots somehow got out of sync; reset the list for sanity
169
				redis.call('del',kSlots,kSlotsNextRelease)
170
			elseif redis.call('lLen',kSlots) == (1*rMaxWorkers - 1) and redis.call('zCard',kWaiting) == 0 then
171
				-- Slot list will be made full; clear it to save space (it re-inits as needed)
172
				-- since nothing is waiting on being unblocked by a push to the list
173
				redis.call('del',kSlots,kSlotsNextRelease)
174
			else
175
				-- Add slot back to pool and update the "next release" time
176
				redis.call('rPush',kSlots,rSlot)
177
				redis.call('zAdd',kSlotsNextRelease,rTime + 30,rSlot)
178
				-- Always keep renewing the expiry on use
179
				redis.call('expireAt',kSlots,math.ceil(rTime + rExpiry))
180
				redis.call('expireAt',kSlotsNextRelease,math.ceil(rTime + rExpiry))
181
			end
182
		end
183
		-- Update an ephemeral list to wake up other clients that can
184
		-- reuse any cached work from this process. Only do this if no
185
		-- slots are currently free (e.g. clients could be waiting).
186
		if 1*rAwakeAll == 1 then
187
			local count = redis.call('zCard',kWaiting)
188
			for i = 1,count do
189
				redis.call('rPush',kWakeup,'w')
190
			end
191
			redis.call('pexpire',kWakeup,1)
192
		end
193
		return 1
194
LUA;
195
		// @codingStandardsIgnoreEnd
196
197
		try {
198
			$conn->luaEval( $script,
199
				[
200
					$this->getSlotListKey(),
201
					$this->getSlotRTimeSetKey(),
202
					$this->getWakeupListKey(),
203
					$this->getWaitSetKey(),
204
					$this->workers,
205
					$this->lockTTL,
206
					$this->slot,
207
					$this->slotTime, // used for CAS-style sanity check
208
					( $this->onRelease === self::AWAKE_ALL ) ? 1 : 0,
209
					microtime( true )
210
				],
211
				4 # number of first argument(s) that are keys
212
			);
213
		} catch ( RedisException $e ) {
0 ignored issues
show
The class RedisException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
214
			return Status::newFatal( 'pool-error-unknown', $e->getMessage() );
215
		}
216
217
		$this->slot = null;
218
		$this->slotTime = null;
219
		$this->onRelease = null;
220
		unset( self::$active[$this->session] );
221
222
		$this->onRelease();
223
224
		return Status::newGood( PoolCounter::RELEASED );
225
	}
226
227
	/**
228
	 * @param int $doWakeup AWAKE_* constant
229
	 * @return Status
230
	 */
231
	protected function waitForSlotOrNotif( $doWakeup ) {
232
		if ( $this->slot !== null ) {
233
			return Status::newGood( PoolCounter::LOCK_HELD ); // already acquired
234
		}
235
236
		$status = $this->getConnection();
237
		if ( !$status->isOK() ) {
238
			return $status;
239
		}
240
		$conn = $status->value;
241
242
		$now = microtime( true );
243
		try {
244
			$slot = $this->initAndPopPoolSlotList( $conn, $now );
245
			if ( ctype_digit( $slot ) ) {
246
				// Pool slot acquired by this process
247
				$slotTime = $now;
248
			} elseif ( $slot === 'QUEUE_FULL' ) {
249
				// Too many processes are waiting for pooled processes to finish
250
				return Status::newGood( PoolCounter::QUEUE_FULL );
251
			} elseif ( $slot === 'QUEUE_WAIT' ) {
252
				// This process is now registered as waiting
253
				$keys = ( $doWakeup == self::AWAKE_ALL )
254
					// Wait for an open slot or wake-up signal (preferring the latter)
255
					? [ $this->getWakeupListKey(), $this->getSlotListKey() ]
256
					// Just wait for an actual pool slot
257
					: [ $this->getSlotListKey() ];
258
259
				$res = $conn->blPop( $keys, $this->timeout );
260
				if ( $res === [] ) {
261
					$conn->zRem( $this->getWaitSetKey(), $this->session ); // no longer waiting
262
					return Status::newGood( PoolCounter::TIMEOUT );
263
				}
264
265
				$slot = $res[1]; // pool slot or "w" for wake-up notifications
266
				$slotTime = microtime( true ); // last microtime() was a few RTTs ago
267
				// Unregister this process as waiting and bump slot "next release" time
268
				$this->registerAcquisitionTime( $conn, $slot, $slotTime );
269
			} else {
270
				return Status::newFatal( 'pool-error-unknown', "Server gave slot '$slot'." );
271
			}
272
		} catch ( RedisException $e ) {
0 ignored issues
show
The class RedisException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
273
			return Status::newFatal( 'pool-error-unknown', $e->getMessage() );
274
		}
275
276
		if ( $slot !== 'w' ) {
277
			$this->slot = $slot;
278
			$this->slotTime = $slotTime;
0 ignored issues
show
Documentation Bug introduced by
The property $slotTime was declared of type integer, but $slotTime is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
279
			$this->onRelease = $doWakeup;
280
			self::$active[$this->session] = $this;
281
		}
282
283
		$this->onAcquire();
284
285
		return Status::newGood( $slot === 'w' ? PoolCounter::DONE : PoolCounter::LOCKED );
286
	}
287
288
	/**
289
	 * @param RedisConnRef $conn
290
	 * @param float $now UNIX timestamp
291
	 * @return string|bool False on failure
292
	 */
293
	protected function initAndPopPoolSlotList( RedisConnRef $conn, $now ) {
294
		static $script =
295
		/** @lang Lua */
296
<<<LUA
297
		local kSlots,kSlotsNextRelease,kSlotWaits = unpack(KEYS)
298
		local rMaxWorkers,rMaxQueue,rTimeout,rExpiry,rSess,rTime = unpack(ARGV)
299
		-- Initialize if the "next release" time sorted-set is empty. The slot key
300
		-- itself is empty if all slots are busy or when nothing is initialized.
301
		-- If the list is empty but the set is not, then it is the latter case.
302
		-- For sanity, if the list exists but not the set, then reset everything.
303
		if redis.call('exists',kSlotsNextRelease) == 0 then
304
			redis.call('del',kSlots)
305
			for i = 1,1*rMaxWorkers do
306
				redis.call('rPush',kSlots,i)
307
				redis.call('zAdd',kSlotsNextRelease,-1,i)
308
			end
309
		-- Otherwise do maintenance to clean up after network partitions
310
		else
311
			-- Find stale slot locks and add free them (avoid duplicates for sanity)
312
			local staleLocks = redis.call('zRangeByScore',kSlotsNextRelease,0,rTime)
313
			for k,slot in ipairs(staleLocks) do
314
				redis.call('lRem',kSlots,0,slot)
315
				redis.call('rPush',kSlots,slot)
316
				redis.call('zAdd',kSlotsNextRelease,rTime + 30,slot)
317
			end
318
			-- Find stale wait slot entries and remove them
319
			redis.call('zRemRangeByScore',kSlotWaits,0,rTime - 2*rTimeout)
320
		end
321
		local slot
322
		-- Try to acquire a slot if possible now
323
		if redis.call('lLen',kSlots) > 0 then
324
			slot = redis.call('lPop',kSlots)
325
			-- Update the slot "next release" time
326
			redis.call('zAdd',kSlotsNextRelease,rTime + rExpiry,slot)
327
		elseif redis.call('zCard',kSlotWaits) >= 1*rMaxQueue then
328
			slot = 'QUEUE_FULL'
329
		else
330
			slot = 'QUEUE_WAIT'
331
			-- Register this process as waiting
332
			redis.call('zAdd',kSlotWaits,rTime,rSess)
333
			redis.call('expireAt',kSlotWaits,math.ceil(rTime + 2*rTimeout))
334
		end
335
		-- Always keep renewing the expiry on use
336
		redis.call('expireAt',kSlots,math.ceil(rTime + rExpiry))
337
		redis.call('expireAt',kSlotsNextRelease,math.ceil(rTime + rExpiry))
338
		return slot
339
LUA;
340
		return $conn->luaEval( $script,
341
			[
342
				$this->getSlotListKey(),
343
				$this->getSlotRTimeSetKey(),
344
				$this->getWaitSetKey(),
345
				$this->workers,
346
				$this->maxqueue,
347
				$this->timeout,
348
				$this->lockTTL,
349
				$this->session,
350
				$now
351
			],
352
			3 # number of first argument(s) that are keys
353
		);
354
	}
355
356
	/**
357
	 * @param RedisConnRef $conn
358
	 * @param string $slot
359
	 * @param float $now
360
	 * @return int|bool False on failure
361
	 */
362
	protected function registerAcquisitionTime( RedisConnRef $conn, $slot, $now ) {
363
		static $script =
364
		/** @lang Lua */
365
<<<LUA
366
		local kSlots,kSlotsNextRelease,kSlotWaits = unpack(KEYS)
367
		local rSlot,rExpiry,rSess,rTime = unpack(ARGV)
368
		-- If rSlot is 'w' then the client was told to wake up but got no slot
369
		if rSlot ~= 'w' then
370
			-- Update the slot "next release" time
371
			redis.call('zAdd',kSlotsNextRelease,rTime + rExpiry,rSlot)
372
			-- Always keep renewing the expiry on use
373
			redis.call('expireAt',kSlots,math.ceil(rTime + rExpiry))
374
			redis.call('expireAt',kSlotsNextRelease,math.ceil(rTime + rExpiry))
375
		end
376
		-- Unregister this process as waiting
377
		redis.call('zRem',kSlotWaits,rSess)
378
		return 1
379
LUA;
380
		return $conn->luaEval( $script,
381
			[
382
				$this->getSlotListKey(),
383
				$this->getSlotRTimeSetKey(),
384
				$this->getWaitSetKey(),
385
				$slot,
386
				$this->lockTTL,
387
				$this->session,
388
				$now
389
			],
390
			3 # number of first argument(s) that are keys
391
		);
392
	}
393
394
	/**
395
	 * @return string
396
	 */
397
	protected function getSlotListKey() {
398
		return "poolcounter:l-slots-{$this->keySha1}-{$this->workers}";
399
	}
400
401
	/**
402
	 * @return string
403
	 */
404
	protected function getSlotRTimeSetKey() {
405
		return "poolcounter:z-renewtime-{$this->keySha1}-{$this->workers}";
406
	}
407
408
	/**
409
	 * @return string
410
	 */
411
	protected function getWaitSetKey() {
412
		return "poolcounter:z-wait-{$this->keySha1}-{$this->workers}";
413
	}
414
415
	/**
416
	 * @return string
417
	 */
418
	protected function getWakeupListKey() {
419
		return "poolcounter:l-wakeup-{$this->keySha1}-{$this->workers}";
420
	}
421
422
	/**
423
	 * Try to make sure that locks get released (even with exceptions and fatals)
424
	 */
425
	public static function releaseAll() {
426
		foreach ( self::$active as $poolCounter ) {
427
			try {
428
				if ( $poolCounter->slot !== null ) {
429
					$poolCounter->release();
430
				}
431
			} catch ( Exception $e ) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
432
			}
433
		}
434
	}
435
}
436