Coverage Report

Created: 2025-07-23 06:33

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