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