Coverage Report

Created: 2025-06-13 06:43

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