Coverage Report

Created: 2026-02-14 06:52

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