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