Coverage Report

Created: 2026-06-02 06:36

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