diff --git a/NEWS b/NEWS index 070a225fcffd2..196ad576f5b31 100644 --- a/NEWS +++ b/NEWS @@ -55,6 +55,9 @@ PHP NEWS - Posix: . Added posix_sysconf. (David Carlier) +- Random: + . Added Randomizer::getBytesFromString(). (Joshua Rüsweg) + - Reflection: . Fix GH-9470 (ReflectionMethod constructor should not find private parent method). (ilutov) diff --git a/UPGRADING b/UPGRADING index 16f2de9eff26b..0956b914b93c7 100644 --- a/UPGRADING +++ b/UPGRADING @@ -62,6 +62,10 @@ PHP 8.3 UPGRADE NOTES - Posix: . Added posix_sysconf call to get runtime informations. +- Random: + . Added Randomizer::getBytesFromString(). + RFC: https://wiki.php.net/rfc/randomizer_additions + - Sockets: . Added socket_atmark to checks if the socket is OOB marked. diff --git a/ext/random/php_random.h b/ext/random/php_random.h index 6a8b0eb84a704..a4665b5d10aca 100644 --- a/ext/random/php_random.h +++ b/ext/random/php_random.h @@ -74,6 +74,8 @@ PHPAPI double php_combined_lcg(void); # define MT_N (624) +#define PHP_RANDOM_RANGE_ATTEMPTS (50) + PHPAPI void php_mt_srand(uint32_t seed); PHPAPI uint32_t php_mt_rand(void); PHPAPI zend_long php_mt_rand_range(zend_long min, zend_long max); diff --git a/ext/random/random.c b/ext/random/random.c index a1a1702e16789..5f6ae0c720681 100644 --- a/ext/random/random.c +++ b/ext/random/random.c @@ -86,8 +86,6 @@ static zend_object_handlers random_engine_xoshiro256starstar_object_handlers; static zend_object_handlers random_engine_secure_object_handlers; static zend_object_handlers random_randomizer_object_handlers; -#define RANDOM_RANGE_ATTEMPTS (50) - static inline uint32_t rand_range32(const php_random_algo *algo, php_random_status *status, uint32_t umax) { uint32_t result, limit; @@ -124,8 +122,8 @@ static inline uint32_t rand_range32(const php_random_algo *algo, php_random_stat /* Discard numbers over the limit to avoid modulo bias */ while (UNEXPECTED(result > limit)) { /* If the requirements cannot be met in a cycles, return fail */ - if (++count > RANDOM_RANGE_ATTEMPTS) { - zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", RANDOM_RANGE_ATTEMPTS); + if (++count > PHP_RANDOM_RANGE_ATTEMPTS) { + zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS); return 0; } @@ -180,8 +178,8 @@ static inline uint64_t rand_range64(const php_random_algo *algo, php_random_stat /* Discard numbers over the limit to avoid modulo bias */ while (UNEXPECTED(result > limit)) { /* If the requirements cannot be met in a cycles, return fail */ - if (++count > RANDOM_RANGE_ATTEMPTS) { - zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", RANDOM_RANGE_ATTEMPTS); + if (++count > PHP_RANDOM_RANGE_ATTEMPTS) { + zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS); return 0; } diff --git a/ext/random/random.stub.php b/ext/random/random.stub.php index 0a178f2657dc2..69049a837b2c2 100644 --- a/ext/random/random.stub.php +++ b/ext/random/random.stub.php @@ -137,6 +137,8 @@ public function getInt(int $min, int $max): int {} public function getBytes(int $length): string {} + public function getBytesFromString(string $string, int $length): string {} + public function shuffleArray(array $array): array {} public function shuffleBytes(string $bytes): string {} diff --git a/ext/random/random_arginfo.h b/ext/random/random_arginfo.h index fc272607360d0..1da1b8576b196 100644 --- a/ext/random/random_arginfo.h +++ b/ext/random/random_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 6cc9022516ce23c2e95af30606db43e9fc28e38a */ + * Stub hash: a4226bc7838eba98c5a935b279f681a7d083c0b2 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_lcg_value, 0, 0, IS_DOUBLE, 0) ZEND_END_ARG_INFO() @@ -94,6 +94,11 @@ ZEND_END_ARG_INFO() #define arginfo_class_Random_Randomizer_getBytes arginfo_random_bytes +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Random_Randomizer_getBytesFromString, 0, 2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, length, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Random_Randomizer_shuffleArray, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) ZEND_END_ARG_INFO() @@ -133,6 +138,7 @@ ZEND_METHOD(Random_Randomizer, __construct); ZEND_METHOD(Random_Randomizer, nextInt); ZEND_METHOD(Random_Randomizer, getInt); ZEND_METHOD(Random_Randomizer, getBytes); +ZEND_METHOD(Random_Randomizer, getBytesFromString); ZEND_METHOD(Random_Randomizer, shuffleArray); ZEND_METHOD(Random_Randomizer, shuffleBytes); ZEND_METHOD(Random_Randomizer, pickArrayKeys); @@ -209,6 +215,7 @@ static const zend_function_entry class_Random_Randomizer_methods[] = { ZEND_ME(Random_Randomizer, nextInt, arginfo_class_Random_Randomizer_nextInt, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, getInt, arginfo_class_Random_Randomizer_getInt, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, getBytes, arginfo_class_Random_Randomizer_getBytes, ZEND_ACC_PUBLIC) + ZEND_ME(Random_Randomizer, getBytesFromString, arginfo_class_Random_Randomizer_getBytesFromString, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, shuffleArray, arginfo_class_Random_Randomizer_shuffleArray, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, shuffleBytes, arginfo_class_Random_Randomizer_shuffleBytes, ZEND_ACC_PUBLIC) ZEND_ME(Random_Randomizer, pickArrayKeys, arginfo_class_Random_Randomizer_pickArrayKeys, ZEND_ACC_PUBLIC) diff --git a/ext/random/randomizer.c b/ext/random/randomizer.c index 391cc16cc74db..a95e6b0fdd8a8 100644 --- a/ext/random/randomizer.c +++ b/ext/random/randomizer.c @@ -131,7 +131,7 @@ PHP_METHOD(Random_Randomizer, getInt) && ((php_random_status_state_mt19937 *) randomizer->status->state)->mode != MT_RAND_MT19937 )) { uint64_t r = php_random_algo_mt19937.generate(randomizer->status) >> 1; - + /* This is an inlined version of the RAND_RANGE_BADSCALING macro that does not invoke UB when encountering * (max - min) > ZEND_LONG_MAX. */ @@ -258,6 +258,102 @@ PHP_METHOD(Random_Randomizer, pickArrayKeys) } /* }}} */ +/* {{{ Get Random Bytes for String */ +PHP_METHOD(Random_Randomizer, getBytesFromString) +{ + php_random_randomizer *randomizer = Z_RANDOM_RANDOMIZER_P(ZEND_THIS); + zend_long length; + zend_string *source, *retval; + size_t total_size = 0; + + ZEND_PARSE_PARAMETERS_START(2, 2); + Z_PARAM_STR(source) + Z_PARAM_LONG(length) + ZEND_PARSE_PARAMETERS_END(); + + const size_t source_length = ZSTR_LEN(source); + + if (source_length < 1) { + zend_argument_value_error(1, "cannot be empty"); + RETURN_THROWS(); + } + + if (length < 1) { + zend_argument_value_error(2, "must be greater than 0"); + RETURN_THROWS(); + } + + retval = zend_string_alloc(length, 0); + + if (source_length > 0xFF) { + while (total_size < length) { + uint64_t offset = randomizer->algo->range(randomizer->status, 0, source_length - 1); + + if (EG(exception)) { + zend_string_free(retval); + RETURN_THROWS(); + } + + ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset]; + } + } else { + uint64_t mask; + if (source_length <= 0x1) { + mask = 0x0; + } else if (source_length <= 0x2) { + mask = 0x1; + } else if (source_length <= 0x4) { + mask = 0x3; + } else if (source_length <= 0x8) { + mask = 0x7; + } else if (source_length <= 0x10) { + mask = 0xF; + } else if (source_length <= 0x20) { + mask = 0x1F; + } else if (source_length <= 0x40) { + mask = 0x3F; + } else if (source_length <= 0x80) { + mask = 0x7F; + } else { + mask = 0xFF; + } + + int failures = 0; + while (total_size < length) { + uint64_t result = randomizer->algo->generate(randomizer->status); + if (EG(exception)) { + zend_string_free(retval); + RETURN_THROWS(); + } + + for (size_t i = 0; i < randomizer->status->last_generated_size; i++) { + uint64_t offset = (result >> (i * 8)) & mask; + + if (offset >= source_length) { + if (++failures > PHP_RANDOM_RANGE_ATTEMPTS) { + zend_string_free(retval); + zend_throw_error(random_ce_Random_BrokenRandomEngineError, "Failed to generate an acceptable random number in %d attempts", PHP_RANDOM_RANGE_ATTEMPTS); + RETURN_THROWS(); + } + + continue; + } + + failures = 0; + + ZSTR_VAL(retval)[total_size++] = ZSTR_VAL(source)[offset]; + if (total_size >= length) { + break; + } + } + } + } + + ZSTR_VAL(retval)[length] = '\0'; + RETURN_STR(retval); +} +/* }}} */ + /* {{{ Random\Randomizer::__serialize() */ PHP_METHOD(Random_Randomizer, __serialize) { diff --git a/ext/random/tests/03_randomizer/engine_unsafe_biased.phpt b/ext/random/tests/03_randomizer/engine_unsafe_biased.phpt index 09fbd85b54eb0..e6fd2d7672343 100644 --- a/ext/random/tests/03_randomizer/engine_unsafe_biased.phpt +++ b/ext/random/tests/03_randomizer/engine_unsafe_biased.phpt @@ -49,6 +49,18 @@ try { echo $e->getMessage(), PHP_EOL; } +try { + var_dump(randomizer()->getBytesFromString('123', 10)); +} catch (Random\BrokenRandomEngineError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + var_dump(randomizer()->getBytesFromString(str_repeat('a', 500), 10)); +} catch (Random\BrokenRandomEngineError $e) { + echo $e->getMessage(), PHP_EOL; +} + ?> --EXPECTF-- Failed to generate an acceptable random number in 50 attempts @@ -56,3 +68,5 @@ int(%d) string(2) "ff" Failed to generate an acceptable random number in 50 attempts Failed to generate an acceptable random number in 50 attempts +Failed to generate an acceptable random number in 50 attempts +Failed to generate an acceptable random number in 50 attempts diff --git a/ext/random/tests/03_randomizer/engine_unsafe_empty_string.phpt b/ext/random/tests/03_randomizer/engine_unsafe_empty_string.phpt index 01bd293bc0508..13ad0637fc33d 100644 --- a/ext/random/tests/03_randomizer/engine_unsafe_empty_string.phpt +++ b/ext/random/tests/03_randomizer/engine_unsafe_empty_string.phpt @@ -49,6 +49,18 @@ try { echo $e->getMessage(), PHP_EOL; } +try { + var_dump(randomizer()->getBytesFromString('123', 10)); +} catch (Random\BrokenRandomEngineError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + var_dump(randomizer()->getBytesFromString(str_repeat('a', 500), 10)); +} catch (Random\BrokenRandomEngineError $e) { + echo $e->getMessage(), PHP_EOL; +} + ?> --EXPECT-- A random engine must return a non-empty string @@ -56,3 +68,5 @@ A random engine must return a non-empty string A random engine must return a non-empty string A random engine must return a non-empty string A random engine must return a non-empty string +A random engine must return a non-empty string +A random engine must return a non-empty string diff --git a/ext/random/tests/03_randomizer/methods/getBytesFromString.phpt b/ext/random/tests/03_randomizer/methods/getBytesFromString.phpt new file mode 100644 index 0000000000000..7ee7d990adff7 --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/getBytesFromString.phpt @@ -0,0 +1,63 @@ +--TEST-- +Random: Randomizer: getBytesFromString(): Basic functionality +--FILE-- +getBytesFromString('a', 10)); + var_dump($randomizer->getBytesFromString(str_repeat('a', 256), 5)); + + for ($i = 1; $i < 250; $i++) { + $output = $randomizer->getBytesFromString(str_repeat('ab', $i), 500); + + // This check can theoretically fail with a chance of 0.5**500. + if (!str_contains($output, 'a') || !str_contains($output, 'b')) { + die("failure: didn't see both a and b at {$i}"); + } + } +} + +die('success'); + +?> +--EXPECT-- +Random\Engine\Mt19937 +string(10) "aaaaaaaaaa" +string(5) "aaaaa" +Random\Engine\Mt19937 +string(10) "aaaaaaaaaa" +string(5) "aaaaa" +Random\Engine\PcgOneseq128XslRr64 +string(10) "aaaaaaaaaa" +string(5) "aaaaa" +Random\Engine\Xoshiro256StarStar +string(10) "aaaaaaaaaa" +string(5) "aaaaa" +Random\Engine\Secure +string(10) "aaaaaaaaaa" +string(5) "aaaaa" +Random\Engine\Test\TestShaEngine +string(10) "aaaaaaaaaa" +string(5) "aaaaa" +success diff --git a/ext/random/tests/03_randomizer/methods/getBytesFromString_error.phpt b/ext/random/tests/03_randomizer/methods/getBytesFromString_error.phpt new file mode 100644 index 0000000000000..7280949d647e8 --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/getBytesFromString_error.phpt @@ -0,0 +1,28 @@ +--TEST-- +Random: Randomizer: getBytesFromString(): Parameters are correctly validated +--FILE-- +getBytesFromString("", 2)); +} catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + var_dump(randomizer()->getBytesFromString("abc", 0)); +} catch (ValueError $e) { + echo $e->getMessage(), PHP_EOL; +} + +?> +--EXPECTF-- +Random\Randomizer::getBytesFromString(): Argument #1 ($string) cannot be empty +Random\Randomizer::getBytesFromString(): Argument #2 ($length) must be greater than 0