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/libs/lockmanager/MemcLockManager.php (4 issues)

Labels
Severity

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
 * Version of LockManager based on using memcached servers.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup LockManager
22
 */
23
use Wikimedia\WaitConditionLoop;
24
25
/**
26
 * Manage locks using memcached servers.
27
 *
28
 * Version of LockManager based on using memcached servers.
29
 * This is meant for multi-wiki systems that may share files.
30
 * All locks are non-blocking, which avoids deadlocks.
31
 *
32
 * All lock requests for a resource, identified by a hash string, will map to one
33
 * bucket. Each bucket maps to one or several peer servers, each running memcached.
34
 * A majority of peers must agree for a lock to be acquired.
35
 *
36
 * @ingroup LockManager
37
 * @since 1.20
38
 */
39
class MemcLockManager extends QuorumLockManager {
40
	/** @var array Mapping of lock types to the type actually used */
41
	protected $lockTypeMap = [
42
		self::LOCK_SH => self::LOCK_SH,
43
		self::LOCK_UW => self::LOCK_SH,
44
		self::LOCK_EX => self::LOCK_EX
45
	];
46
47
	/** @var MemcachedBagOStuff[] Map of (server name => MemcachedBagOStuff) */
48
	protected $cacheServers = [];
49
	/** @var HashBagOStuff Server status cache */
50
	protected $statusCache;
51
52
	/**
53
	 * Construct a new instance from configuration.
54
	 *
55
	 * @param array $config Parameters include:
56
	 *   - lockServers  : Associative array of server names to "<IP>:<port>" strings.
57
	 *   - srvsByBucket : Array of 1-16 consecutive integer keys, starting from 0,
58
	 *                    each having an odd-numbered list of server names (peers) as values.
59
	 *   - memcConfig   : Configuration array for MemcachedBagOStuff::construct() with an
60
	 *                    additional 'class' parameter specifying which MemcachedBagOStuff
61
	 *                    subclass to use. The server names will be injected. [optional]
62
	 * @throws Exception
63
	 */
64
	public function __construct( array $config ) {
65
		parent::__construct( $config );
66
67
		// Sanitize srvsByBucket config to prevent PHP errors
68
		$this->srvsByBucket = array_filter( $config['srvsByBucket'], 'is_array' );
69
		$this->srvsByBucket = array_values( $this->srvsByBucket ); // consecutive
70
71
		$memcConfig = isset( $config['memcConfig'] ) ? $config['memcConfig'] : [];
72
		$memcConfig += [ 'class' => 'MemcachedPhpBagOStuff' ]; // default
73
74
		$class = $memcConfig['class'];
75
		if ( !is_subclass_of( $class, 'MemcachedBagOStuff' ) ) {
76
			throw new InvalidArgumentException( "$class is not of type MemcachedBagOStuff." );
77
		}
78
79
		foreach ( $config['lockServers'] as $name => $address ) {
80
			$params = [ 'servers' => [ $address ] ] + $memcConfig;
81
			$this->cacheServers[$name] = new $class( $params );
82
		}
83
84
		$this->statusCache = new HashBagOStuff();
85
	}
86
87
	protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
88
		$status = StatusValue::newGood();
89
90
		$memc = $this->getCache( $lockSrv );
91
		// List of affected paths
92
		$paths = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
93
		$paths = array_unique( $paths );
94
		// List of affected lock record keys
95
		$keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
96
97
		// Lock all of the active lock record keys...
98
		if ( !$this->acquireMutexes( $memc, $keys ) ) {
0 ignored issues
show
It seems like $memc defined by $this->getCache($lockSrv) on line 90 can be null; however, MemcLockManager::acquireMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
99
			foreach ( $paths as $path ) {
100
				$status->fatal( 'lockmanager-fail-acquirelock', $path );
101
			}
102
103
			return $status;
104
		}
105
106
		// Fetch all the existing lock records...
107
		$lockRecords = $memc->getMulti( $keys );
108
109
		$now = time();
110
		// Check if the requested locks conflict with existing ones...
111
		foreach ( $pathsByType as $type => $paths ) {
112
			foreach ( $paths as $path ) {
113
				$locksKey = $this->recordKeyForPath( $path );
114
				$locksHeld = isset( $lockRecords[$locksKey] )
115
					? self::sanitizeLockArray( $lockRecords[$locksKey] )
116
					: self::newLockArray(); // init
117 View Code Duplication
				foreach ( $locksHeld[self::LOCK_EX] as $session => $expiry ) {
118
					if ( $expiry < $now ) { // stale?
119
						unset( $locksHeld[self::LOCK_EX][$session] );
120
					} elseif ( $session !== $this->session ) {
121
						$status->fatal( 'lockmanager-fail-acquirelock', $path );
122
					}
123
				}
124
				if ( $type === self::LOCK_EX ) {
125 View Code Duplication
					foreach ( $locksHeld[self::LOCK_SH] as $session => $expiry ) {
126
						if ( $expiry < $now ) { // stale?
127
							unset( $locksHeld[self::LOCK_SH][$session] );
128
						} elseif ( $session !== $this->session ) {
129
							$status->fatal( 'lockmanager-fail-acquirelock', $path );
130
						}
131
					}
132
				}
133
				if ( $status->isOK() ) {
134
					// Register the session in the lock record array
135
					$locksHeld[$type][$this->session] = $now + $this->lockTTL;
136
					// We will update this record if none of the other locks conflict
137
					$lockRecords[$locksKey] = $locksHeld;
138
				}
139
			}
140
		}
141
142
		// If there were no lock conflicts, update all the lock records...
143
		if ( $status->isOK() ) {
144
			foreach ( $paths as $path ) {
145
				$locksKey = $this->recordKeyForPath( $path );
146
				$locksHeld = $lockRecords[$locksKey];
147
				$ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
148
				if ( !$ok ) {
149
					$status->fatal( 'lockmanager-fail-acquirelock', $path );
150
				} else {
151
					$this->logger->debug( __METHOD__ . ": acquired lock on key $locksKey.\n" );
152
				}
153
			}
154
		}
155
156
		// Unlock all of the active lock record keys...
157
		$this->releaseMutexes( $memc, $keys );
0 ignored issues
show
It seems like $memc defined by $this->getCache($lockSrv) on line 90 can be null; however, MemcLockManager::releaseMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
158
159
		return $status;
160
	}
161
162
	protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
163
		$status = StatusValue::newGood();
164
165
		$memc = $this->getCache( $lockSrv );
166
		// List of affected paths
167
		$paths = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
168
		$paths = array_unique( $paths );
169
		// List of affected lock record keys
170
		$keys = array_map( [ $this, 'recordKeyForPath' ], $paths );
171
172
		// Lock all of the active lock record keys...
173
		if ( !$this->acquireMutexes( $memc, $keys ) ) {
0 ignored issues
show
It seems like $memc defined by $this->getCache($lockSrv) on line 165 can be null; however, MemcLockManager::acquireMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
174
			foreach ( $paths as $path ) {
175
				$status->fatal( 'lockmanager-fail-releaselock', $path );
176
			}
177
178
			return $status;
179
		}
180
181
		// Fetch all the existing lock records...
182
		$lockRecords = $memc->getMulti( $keys );
183
184
		// Remove the requested locks from all records...
185
		foreach ( $pathsByType as $type => $paths ) {
186
			foreach ( $paths as $path ) {
187
				$locksKey = $this->recordKeyForPath( $path ); // lock record
188
				if ( !isset( $lockRecords[$locksKey] ) ) {
189
					$status->warning( 'lockmanager-fail-releaselock', $path );
190
					continue; // nothing to do
191
				}
192
				$locksHeld = $this->sanitizeLockArray( $lockRecords[$locksKey] );
193
				if ( isset( $locksHeld[$type][$this->session] ) ) {
194
					unset( $locksHeld[$type][$this->session] ); // unregister this session
195
					$lockRecords[$locksKey] = $locksHeld;
196
				} else {
197
					$status->warning( 'lockmanager-fail-releaselock', $path );
198
				}
199
			}
200
		}
201
202
		// Persist the new lock record values...
203
		foreach ( $paths as $path ) {
204
			$locksKey = $this->recordKeyForPath( $path );
205
			if ( !isset( $lockRecords[$locksKey] ) ) {
206
				continue; // nothing to do
207
			}
208
			$locksHeld = $lockRecords[$locksKey];
209
			if ( $locksHeld === $this->newLockArray() ) {
210
				$ok = $memc->delete( $locksKey );
211
			} else {
212
				$ok = $memc->set( $locksKey, $locksHeld, self::MAX_LOCK_TTL );
213
			}
214
			if ( $ok ) {
215
				$this->logger->debug( __METHOD__ . ": released lock on key $locksKey.\n" );
216
			} else {
217
				$status->fatal( 'lockmanager-fail-releaselock', $path );
218
			}
219
		}
220
221
		// Unlock all of the active lock record keys...
222
		$this->releaseMutexes( $memc, $keys );
0 ignored issues
show
It seems like $memc defined by $this->getCache($lockSrv) on line 165 can be null; however, MemcLockManager::releaseMutexes() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
223
224
		return $status;
225
	}
226
227
	/**
228
	 * @see QuorumLockManager::releaseAllLocks()
229
	 * @return StatusValue
230
	 */
231
	protected function releaseAllLocks() {
232
		return StatusValue::newGood(); // not supported
233
	}
234
235
	/**
236
	 * @see QuorumLockManager::isServerUp()
237
	 * @param string $lockSrv
238
	 * @return bool
239
	 */
240
	protected function isServerUp( $lockSrv ) {
241
		return (bool)$this->getCache( $lockSrv );
242
	}
243
244
	/**
245
	 * Get the MemcachedBagOStuff object for a $lockSrv
246
	 *
247
	 * @param string $lockSrv Server name
248
	 * @return MemcachedBagOStuff|null
249
	 */
250
	protected function getCache( $lockSrv ) {
251
		if ( !isset( $this->cacheServers[$lockSrv] ) ) {
252
			throw new InvalidArgumentException( "Invalid cache server '$lockSrv'." );
253
		}
254
255
		$online = $this->statusCache->get( "online:$lockSrv" );
256
		if ( $online === false ) {
257
			$online = $this->cacheServers[$lockSrv]->set( __CLASS__ . ':ping', 1, 1 );
258
			if ( !$online ) { // server down?
259
				$this->logger->warning( __METHOD__ . ": Could not contact $lockSrv." );
260
			}
261
			$this->statusCache->set( "online:$lockSrv", (int)$online, 30 );
262
		}
263
264
		return $online ? $this->cacheServers[$lockSrv] : null;
265
	}
266
267
	/**
268
	 * @param string $path
269
	 * @return string
270
	 */
271
	protected function recordKeyForPath( $path ) {
272
		return implode( ':', [ __CLASS__, 'locks', $this->sha1Base36Absolute( $path ) ] );
273
	}
274
275
	/**
276
	 * @return array An empty lock structure for a key
277
	 */
278
	protected function newLockArray() {
279
		return [ self::LOCK_SH => [], self::LOCK_EX => [] ];
280
	}
281
282
	/**
283
	 * @param array $a
284
	 * @return array An empty lock structure for a key
285
	 */
286
	protected function sanitizeLockArray( $a ) {
287
		if ( is_array( $a ) && isset( $a[self::LOCK_EX] ) && isset( $a[self::LOCK_SH] ) ) {
288
			return $a;
289
		}
290
291
		$this->logger->error( __METHOD__ . ": reset invalid lock array." );
292
293
		return $this->newLockArray();
294
	}
295
296
	/**
297
	 * @param MemcachedBagOStuff $memc
298
	 * @param array $keys List of keys to acquire
299
	 * @return bool
300
	 */
301
	protected function acquireMutexes( MemcachedBagOStuff $memc, array $keys ) {
302
		$lockedKeys = [];
303
304
		// Acquire the keys in lexicographical order, to avoid deadlock problems.
305
		// If P1 is waiting to acquire a key P2 has, P2 can't also be waiting for a key P1 has.
306
		sort( $keys );
307
308
		// Try to quickly loop to acquire the keys, but back off after a few rounds.
309
		// This reduces memcached spam, especially in the rare case where a server acquires
310
		// some lock keys and dies without releasing them. Lock keys expire after a few minutes.
311
		$loop = new WaitConditionLoop(
312
			function () use ( $memc, $keys, &$lockedKeys ) {
313
				foreach ( array_diff( $keys, $lockedKeys ) as $key ) {
314
					if ( $memc->add( "$key:mutex", 1, 180 ) ) { // lock record
315
						$lockedKeys[] = $key;
316
					}
317
				}
318
319
				return array_diff( $keys, $lockedKeys )
320
					? WaitConditionLoop::CONDITION_CONTINUE
321
					: true;
322
			},
323
			3.0 // timeout
324
		);
325
		$loop->invoke();
326
327
		if ( count( $lockedKeys ) != count( $keys ) ) {
328
			$this->releaseMutexes( $memc, $lockedKeys ); // failed; release what was locked
329
			return false;
330
		}
331
332
		return true;
333
	}
334
335
	/**
336
	 * @param MemcachedBagOStuff $memc
337
	 * @param array $keys List of acquired keys
338
	 */
339
	protected function releaseMutexes( MemcachedBagOStuff $memc, array $keys ) {
340
		foreach ( $keys as $key ) {
341
			$memc->delete( "$key:mutex" );
342
		}
343
	}
344
345
	/**
346
	 * Make sure remaining locks get cleared for sanity
347
	 */
348
	function __destruct() {
349
		while ( count( $this->locksHeld ) ) {
350
			foreach ( $this->locksHeld as $path => $locks ) {
351
				$this->doUnlock( [ $path ], self::LOCK_EX );
352
				$this->doUnlock( [ $path ], self::LOCK_SH );
353
			}
354
		}
355
	}
356
}
357