xref: /php-src/ext/random/randomizer.c (revision 380f8548)
1 /*
2    +----------------------------------------------------------------------+
3    | Copyright (c) The PHP Group                                          |
4    +----------------------------------------------------------------------+
5    | This source file is subject to version 3.01 of the PHP license,      |
6    | that is bundled with this package in the file LICENSE, and is        |
7    | available through the world-wide-web at the following url:           |
8    | https://www.php.net/license/3_01.txt                                 |
9    | If you did not receive a copy of the PHP license and are unable to   |
10    | obtain it through the world-wide-web, please send a note to          |
11    | license@php.net so we can mail you a copy immediately.               |
12    +----------------------------------------------------------------------+
13    | Author: Go Kudo <zeriyoshi@php.net>                                  |
14    +----------------------------------------------------------------------+
15 */
16 
17 #ifdef HAVE_CONFIG_H
18 # include "config.h"
19 #endif
20 
21 #include "php.h"
22 #include "php_random.h"
23 
24 #include "ext/standard/php_array.h"
25 #include "ext/standard/php_string.h"
26 
27 #include "Zend/zend_enum.h"
28 #include "Zend/zend_exceptions.h"
29 #include "zend_portability.h"
30 
randomizer_common_init(php_random_randomizer * randomizer,zend_object * engine_object)31 static inline void randomizer_common_init(php_random_randomizer *randomizer, zend_object *engine_object) {
32 	if (engine_object->ce->type == ZEND_INTERNAL_CLASS) {
33 		/* Internal classes always php_random_engine struct */
34 		php_random_engine *engine = php_random_engine_from_obj(engine_object);
35 
36 		/* Copy engine pointers */
37 		randomizer->engine = engine->engine;
38 	} else {
39 		/* Self allocation */
40 		php_random_status_state_user *state = php_random_status_alloc(&php_random_algo_user, false);
41 		randomizer->engine = (php_random_algo_with_state){
42 			.algo = &php_random_algo_user,
43 			.state = state,
44 		};
45 
46 		zend_string *mname;
47 		zend_function *generate_method;
48 
49 		mname = ZSTR_INIT_LITERAL("generate", 0);
50 		generate_method = zend_hash_find_ptr(&engine_object->ce->function_table, mname);
51 		zend_string_release(mname);
52 
53 		/* Create compatible state */
54 		state->object = engine_object;
55 		state->generate_method = generate_method;
56 
57 		/* Mark self-allocated for memory management */
58 		randomizer->is_userland_algo = true;
59 	}
60 }
61 
62 /* {{{ Random\Randomizer::__construct() */
PHP_METHOD(Random_Randomizer,__construct)63 PHP_METHOD(Random_Randomizer, __construct)
64 {
65 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
66 	zval engine;
67 	zval *param_engine = NULL;
68 
69 	ZEND_PARSE_PARAMETERS_START(0, 1)
70 		Z_PARAM_OPTIONAL
71 		Z_PARAM_OBJECT_OF_CLASS_OR_NULL(param_engine, random_ce_Random_Engine);
72 	ZEND_PARSE_PARAMETERS_END();
73 
74 	if (param_engine != NULL) {
75 		ZVAL_COPY(&engine, param_engine);
76 	} else {
77 		/* Create default RNG instance */
78 		object_init_ex(&engine, random_ce_Random_Engine_Secure);
79 	}
80 
81 	zend_update_property(random_ce_Random_Randomizer, Z_OBJ_P(ZEND_THIS), "engine", strlen("engine"), &engine);
82 
83 	OBJ_RELEASE(Z_OBJ_P(&engine));
84 
85 	if (EG(exception)) {
86 		RETURN_THROWS();
87 	}
88 
89 	randomizer_common_init(randomizer, Z_OBJ_P(&engine));
90 }
91 /* }}} */
92 
93 /* {{{ Generate a float in [0, 1) */
PHP_METHOD(Random_Randomizer,nextFloat)94 PHP_METHOD(Random_Randomizer, nextFloat)
95 {
96 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
97 	php_random_algo_with_state engine = randomizer->engine;
98 
99 	uint64_t result;
100 	size_t total_size;
101 
102 	ZEND_PARSE_PARAMETERS_NONE();
103 
104 	result = 0;
105 	total_size = 0;
106 	do {
107 		php_random_result r = engine.algo->generate(engine.state);
108 		result = result | (r.result << (total_size * 8));
109 		total_size += r.size;
110 		if (EG(exception)) {
111 			RETURN_THROWS();
112 		}
113 	} while (total_size < sizeof(uint64_t));
114 
115 	/* A double has 53 bits of precision, thus we must not
116 	 * use the full 64 bits of the uint64_t, because we would
117 	 * introduce a bias / rounding error.
118 	 */
119 #if DBL_MANT_DIG != 53
120 # error "Random_Randomizer::nextFloat(): Requires DBL_MANT_DIG == 53 to work."
121 #endif
122 	const double step_size = 1.0 / (1ULL << 53);
123 
124 	/* Use the upper 53 bits, because some engine's lower bits
125 	 * are of lower quality.
126 	 */
127 	result = (result >> 11);
128 
129 	RETURN_DOUBLE(step_size * result);
130 }
131 /* }}} */
132 
133 /* {{{ Generates a random float within a configurable interval.
134  *
135  * This method uses the γ-section algorithm by Frédéric Goualard.
136  */
PHP_METHOD(Random_Randomizer,getFloat)137 PHP_METHOD(Random_Randomizer, getFloat)
138 {
139 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
140 	double min, max;
141 	zend_object *bounds = NULL;
142 	int bounds_type = 'C' + sizeof("ClosedOpen") - 1;
143 
144 	ZEND_PARSE_PARAMETERS_START(2, 3)
145 		Z_PARAM_DOUBLE(min)
146 		Z_PARAM_DOUBLE(max)
147 		Z_PARAM_OPTIONAL
148 		Z_PARAM_OBJ_OF_CLASS(bounds, random_ce_Random_IntervalBoundary);
149 	ZEND_PARSE_PARAMETERS_END();
150 
151 	if (!zend_finite(min)) {
152 		zend_argument_value_error(1, "must be finite");
153 		RETURN_THROWS();
154 	}
155 
156 	if (!zend_finite(max)) {
157 		zend_argument_value_error(2, "must be finite");
158 		RETURN_THROWS();
159 	}
160 
161 	if (bounds) {
162 		zval *case_name = zend_enum_fetch_case_name(bounds);
163 		zend_string *bounds_name = Z_STR_P(case_name);
164 
165 		bounds_type = ZSTR_VAL(bounds_name)[0] + ZSTR_LEN(bounds_name);
166 	}
167 
168 	switch (bounds_type) {
169 	case 'C' + sizeof("ClosedOpen") - 1:
170 		if (UNEXPECTED(max <= min)) {
171 			zend_argument_value_error(2, "must be greater than argument #1 ($min)");
172 			RETURN_THROWS();
173 		}
174 
175 		RETURN_DOUBLE(php_random_gammasection_closed_open(randomizer->engine, min, max));
176 	case 'C' + sizeof("ClosedClosed") - 1:
177 		if (UNEXPECTED(max < min)) {
178 			zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)");
179 			RETURN_THROWS();
180 		}
181 
182 		RETURN_DOUBLE(php_random_gammasection_closed_closed(randomizer->engine, min, max));
183 	case 'O' + sizeof("OpenClosed") - 1:
184 		if (UNEXPECTED(max <= min)) {
185 			zend_argument_value_error(2, "must be greater than argument #1 ($min)");
186 			RETURN_THROWS();
187 		}
188 
189 		RETURN_DOUBLE(php_random_gammasection_open_closed(randomizer->engine, min, max));
190 	case 'O' + sizeof("OpenOpen") - 1:
191 		if (UNEXPECTED(max <= min)) {
192 			zend_argument_value_error(2, "must be greater than argument #1 ($min)");
193 			RETURN_THROWS();
194 		}
195 
196 		RETVAL_DOUBLE(php_random_gammasection_open_open(randomizer->engine, min, max));
197 
198 		if (UNEXPECTED(isnan(Z_DVAL_P(return_value)))) {
199 			zend_value_error("The given interval is empty, there are no floats between argument #1 ($min) and argument #2 ($max).");
200 			RETURN_THROWS();
201 		}
202 
203 		return;
204 	default:
205 		ZEND_UNREACHABLE();
206 	}
207 }
208 /* }}} */
209 
210 /* {{{ Generate positive random number */
PHP_METHOD(Random_Randomizer,nextInt)211 PHP_METHOD(Random_Randomizer, nextInt)
212 {
213 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
214 	php_random_algo_with_state engine = randomizer->engine;
215 
216 	ZEND_PARSE_PARAMETERS_NONE();
217 
218 	php_random_result result = engine.algo->generate(engine.state);
219 	if (EG(exception)) {
220 		RETURN_THROWS();
221 	}
222 	if (result.size > sizeof(zend_long)) {
223 		zend_throw_exception(random_ce_Random_RandomException, "Generated value exceeds size of int", 0);
224 		RETURN_THROWS();
225 	}
226 
227 	RETURN_LONG((zend_long) (result.result >> 1));
228 }
229 /* }}} */
230 
231 /* {{{ Generate random number in range */
PHP_METHOD(Random_Randomizer,getInt)232 PHP_METHOD(Random_Randomizer, getInt)
233 {
234 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
235 	php_random_algo_with_state engine = randomizer->engine;
236 
237 	uint64_t result;
238 	zend_long min, max;
239 
240 	ZEND_PARSE_PARAMETERS_START(2, 2)
241 		Z_PARAM_LONG(min)
242 		Z_PARAM_LONG(max)
243 	ZEND_PARSE_PARAMETERS_END();
244 
245 	if (UNEXPECTED(max < min)) {
246 		zend_argument_value_error(2, "must be greater than or equal to argument #1 ($min)");
247 		RETURN_THROWS();
248 	}
249 
250 	if (UNEXPECTED(
251 		engine.algo->range == php_random_algo_mt19937.range
252 		&& ((php_random_status_state_mt19937 *) engine.state)->mode != MT_RAND_MT19937
253 	)) {
254 		uint64_t r = php_random_algo_mt19937.generate(engine.state).result >> 1;
255 
256 		/* This is an inlined version of the RAND_RANGE_BADSCALING macro that does not invoke UB when encountering
257 		 * (max - min) > ZEND_LONG_MAX.
258 		 */
259 		zend_ulong offset = (double) ( (double) max - min + 1.0) * (r / (PHP_MT_RAND_MAX + 1.0));
260 
261 		result = (zend_long) (offset + min);
262 	} else {
263 		result = engine.algo->range(engine.state, min, max);
264 	}
265 
266 	if (EG(exception)) {
267 		RETURN_THROWS();
268 	}
269 
270 	RETURN_LONG((zend_long) result);
271 }
272 /* }}} */
273 
274 /* {{{ Generate random bytes string in ordered length */
PHP_METHOD(Random_Randomizer,getBytes)275 PHP_METHOD(Random_Randomizer, getBytes)
276 {
277 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
278 	php_random_algo_with_state engine = randomizer->engine;
279 
280 	zend_string *retval;
281 	zend_long user_length;
282 	size_t total_size = 0;
283 
284 	ZEND_PARSE_PARAMETERS_START(1, 1)
285 		Z_PARAM_LONG(user_length)
286 	ZEND_PARSE_PARAMETERS_END();
287 
288 	if (user_length < 1) {
289 		zend_argument_value_error(1, "must be greater than 0");
290 		RETURN_THROWS();
291 	}
292 
293 	size_t length = (size_t)user_length;
294 	retval = zend_string_alloc(length, 0);
295 
296 	php_random_result result;
297 	while (total_size + 8 <= length) {
298 		result = engine.algo->generate(engine.state);
299 		if (EG(exception)) {
300 			zend_string_free(retval);
301 			RETURN_THROWS();
302 		}
303 
304 		/* If the result is not 64 bits, we can't use the fast path and
305 		 * we don't attempt to use it in the future, because we don't
306 		 * expect engines to change their output size.
307 		 *
308 		 * While it would be possible to always memcpy() the entire output,
309 		 * using result.size as the length that would result in much worse
310 		 * assembly, because it will actually emit a call to memcpy()
311 		 * instead of just storing the 64 bit value at a memory offset.
312 		 */
313 		if (result.size != 8) {
314 			goto non_64;
315 		}
316 
317 #ifdef WORDS_BIGENDIAN
318 		uint64_t swapped = ZEND_BYTES_SWAP64(result.result);
319 		memcpy(ZSTR_VAL(retval) + total_size, &swapped, 8);
320 #else
321 		memcpy(ZSTR_VAL(retval) + total_size, &result.result, 8);
322 #endif
323 		total_size += 8;
324 	}
325 
326 	while (total_size < length) {
327 		result = engine.algo->generate(engine.state);
328 		if (EG(exception)) {
329 			zend_string_free(retval);
330 			RETURN_THROWS();
331 		}
332 
333  non_64:
334 
335 		for (size_t i = 0; i < result.size; i++) {
336 			ZSTR_VAL(retval)[total_size++] = result.result & 0xff;
337 			result.result >>= 8;
338 			if (total_size >= length) {
339 				break;
340 			}
341 		}
342 	}
343 
344 	ZSTR_VAL(retval)[length] = '\0';
345 	RETURN_STR(retval);
346 }
347 /* }}} */
348 
349 /* {{{ Shuffling array */
PHP_METHOD(Random_Randomizer,shuffleArray)350 PHP_METHOD(Random_Randomizer, shuffleArray)
351 {
352 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
353 	zval *array;
354 
355 	ZEND_PARSE_PARAMETERS_START(1, 1)
356 		Z_PARAM_ARRAY(array)
357 	ZEND_PARSE_PARAMETERS_END();
358 
359 	RETVAL_ARR(zend_array_dup(Z_ARRVAL_P(array)));
360 	if (!php_array_data_shuffle(randomizer->engine, return_value)) {
361 		RETURN_THROWS();
362 	}
363 }
364 /* }}} */
365 
366 /* {{{ Shuffling binary */
PHP_METHOD(Random_Randomizer,shuffleBytes)367 PHP_METHOD(Random_Randomizer, shuffleBytes)
368 {
369 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
370 	zend_string *bytes;
371 
372 	ZEND_PARSE_PARAMETERS_START(1, 1)
373 		Z_PARAM_STR(bytes)
374 	ZEND_PARSE_PARAMETERS_END();
375 
376 	if (ZSTR_LEN(bytes) < 2) {
377 		RETURN_STR_COPY(bytes);
378 	}
379 
380 	RETVAL_STRINGL(ZSTR_VAL(bytes), ZSTR_LEN(bytes));
381 	if (!php_binary_string_shuffle(randomizer->engine, Z_STRVAL_P(return_value), (zend_long) Z_STRLEN_P(return_value))) {
382 		RETURN_THROWS();
383 	}
384 }
385 /* }}} */
386 
387 /* {{{ Pick keys */
PHP_METHOD(Random_Randomizer,pickArrayKeys)388 PHP_METHOD(Random_Randomizer, pickArrayKeys)
389 {
390 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
391 	zval *input, t;
392 	zend_long num_req;
393 
394 	ZEND_PARSE_PARAMETERS_START(2, 2);
395 		Z_PARAM_ARRAY(input)
396 		Z_PARAM_LONG(num_req)
397 	ZEND_PARSE_PARAMETERS_END();
398 
399 	if (!php_array_pick_keys(
400 		randomizer->engine,
401 		input,
402 		num_req,
403 		return_value,
404 		false)
405 	) {
406 		RETURN_THROWS();
407 	}
408 
409 	/* Keep compatibility, But the result is always an array */
410 	if (Z_TYPE_P(return_value) != IS_ARRAY) {
411 		ZVAL_COPY_VALUE(&t, return_value);
412 		array_init(return_value);
413 		zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &t);
414 	}
415 }
416 /* }}} */
417 
418 /* {{{ Get Random Bytes for String */
PHP_METHOD(Random_Randomizer,getBytesFromString)419 PHP_METHOD(Random_Randomizer, getBytesFromString)
420 {
421 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
422 	php_random_algo_with_state engine = randomizer->engine;
423 
424 	zend_long user_length;
425 	zend_string *source, *retval;
426 	size_t total_size = 0;
427 
428 	ZEND_PARSE_PARAMETERS_START(2, 2);
429 		Z_PARAM_STR(source)
430 		Z_PARAM_LONG(user_length)
431 	ZEND_PARSE_PARAMETERS_END();
432 
433 	const size_t source_length = ZSTR_LEN(source);
434 	const size_t max_offset = source_length - 1;
435 
436 	if (source_length < 1) {
437 		zend_argument_must_not_be_empty_error(1);
438 		RETURN_THROWS();
439 	}
440 
441 	if (user_length < 1) {
442 		zend_argument_value_error(2, "must be greater than 0");
443 		RETURN_THROWS();
444 	}
445 
446 	size_t length = (size_t)user_length;
447 	retval = zend_string_alloc(length, 0);
448 
449 	if (max_offset > 0xff) {
450 		while (total_size < length) {
451 			uint64_t offset = engine.algo->range(engine.state, 0, max_offset);
452 
453 			if (EG(exception)) {
454 				zend_string_free(retval);
455 				RETURN_THROWS();
456 			}
457 
458 			ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
459 		}
460 	} else {
461 		uint64_t mask = max_offset;
462 		// Copy the top-most bit into all lower bits.
463 		// Shifting by 4 is sufficient, because max_offset
464 		// is guaranteed to fit in an 8-bit integer at this
465 		// point.
466 		mask |= mask >> 1;
467 		mask |= mask >> 2;
468 		mask |= mask >> 4;
469 		// Expand the lowest byte into all bytes.
470 		mask *= 0x0101010101010101;
471 
472 		int failures = 0;
473 		while (total_size < length) {
474 			php_random_result result = engine.algo->generate(engine.state);
475 			if (EG(exception)) {
476 				zend_string_free(retval);
477 				RETURN_THROWS();
478 			}
479 
480 			uint64_t offsets = result.result & mask;
481 			for (size_t i = 0; i < result.size; i++) {
482 				uint64_t offset = offsets & 0xff;
483 				offsets >>= 8;
484 
485 				if (offset > max_offset) {
486 					if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) {
487 						zend_string_free(retval);
488 						zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS);
489 						RETURN_THROWS();
490 					}
491 
492 					continue;
493 				}
494 
495 				failures = 0;
496 
497 				ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset];
498 				if (total_size >= length) {
499 					break;
500 				}
501 			}
502 		}
503 	}
504 
505 	ZSTR_VAL(retval)[length] = '\0';
506 	RETURN_STR(retval);
507 }
508 /* }}} */
509 
510 /* {{{ Random\Randomizer::__serialize() */
PHP_METHOD(Random_Randomizer,__serialize)511 PHP_METHOD(Random_Randomizer, __serialize)
512 {
513 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
514 	zval t;
515 
516 	ZEND_PARSE_PARAMETERS_NONE();
517 
518 	array_init(return_value);
519 	ZVAL_ARR(&t, zend_std_get_properties(&randomizer->std));
520 	Z_TRY_ADDREF(t);
521 	zend_hash_next_index_insert(Z_ARRVAL_P(return_value), &t);
522 }
523 /* }}} */
524 
525 /* {{{ Random\Randomizer::__unserialize() */
PHP_METHOD(Random_Randomizer,__unserialize)526 PHP_METHOD(Random_Randomizer, __unserialize)
527 {
528 	php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS);
529 	HashTable *d;
530 	zval *members_zv;
531 	zval *zengine;
532 
533 	ZEND_PARSE_PARAMETERS_START(1, 1)
534 		Z_PARAM_ARRAY_HT(d);
535 	ZEND_PARSE_PARAMETERS_END();
536 
537 	/* Verify the expected number of elements, this implicitly ensures that no additional elements are present. */
538 	if (zend_hash_num_elements(d) != 1) {
539 		zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
540 		RETURN_THROWS();
541 	}
542 
543 	members_zv = zend_hash_index_find(d, 0);
544 	if (!members_zv || Z_TYPE_P(members_zv) != IS_ARRAY) {
545 		zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
546 		RETURN_THROWS();
547 	}
548 	object_properties_load(&randomizer->std, Z_ARRVAL_P(members_zv));
549 	if (EG(exception)) {
550 		zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
551 		RETURN_THROWS();
552 	}
553 
554 	zengine = zend_read_property(randomizer->std.ce, &randomizer->std, "engine", strlen("engine"), 1, NULL);
555 	if (Z_TYPE_P(zengine) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(zengine), random_ce_Random_Engine)) {
556 		zend_throw_exception(NULL, "Invalid serialization data for Random\\Randomizer object", 0);
557 		RETURN_THROWS();
558 	}
559 
560 	randomizer_common_init(randomizer, Z_OBJ_P(zengine));
561 }
562 /* }}} */
563