Coverage Report

Created: 2024-01-18 09:20

/src/php-src/ext/standard/random.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
   | Authors: Sammy Kaye Powers <me@sammyk.me>                            |
14
   +----------------------------------------------------------------------+
15
*/
16
17
#include <stdlib.h>
18
#include <sys/stat.h>
19
#include <fcntl.h>
20
#include <math.h>
21
22
#include "php.h"
23
#include "zend_exceptions.h"
24
#include "php_random.h"
25
26
#ifdef PHP_WIN32
27
# include "win32/winutil.h"
28
#endif
29
#ifdef __linux__
30
# include <sys/syscall.h>
31
#endif
32
#if HAVE_SYS_PARAM_H
33
# include <sys/param.h>
34
# if (__FreeBSD__ && __FreeBSD_version > 1200000) || (__DragonFly__ && __DragonFly_version >= 500700) || defined(__sun)
35
#  include <sys/random.h>
36
# endif
37
#endif
38
#if HAVE_COMMONCRYPTO_COMMONRANDOM_H
39
# include <CommonCrypto/CommonCryptoError.h>
40
# include <CommonCrypto/CommonRandom.h>
41
#endif
42
43
#if __has_feature(memory_sanitizer)
44
# include <sanitizer/msan_interface.h>
45
#endif
46
47
#ifdef ZTS
48
int random_globals_id;
49
#else
50
php_random_globals random_globals;
51
#endif
52
53
static void random_globals_ctor(php_random_globals *random_globals_p)
54
1.48k
{
55
1.48k
  random_globals_p->fd = -1;
56
1.48k
}
57
58
static void random_globals_dtor(php_random_globals *random_globals_p)
59
0
{
60
0
  if (random_globals_p->fd > 0) {
61
0
    close(random_globals_p->fd);
62
0
    random_globals_p->fd = -1;
63
0
  }
64
0
}
65
66
/* {{{ */
67
PHP_MINIT_FUNCTION(random)
68
1.48k
{
69
#ifdef ZTS
70
  ts_allocate_id(&random_globals_id, sizeof(php_random_globals), (ts_allocate_ctor)random_globals_ctor, (ts_allocate_dtor)random_globals_dtor);
71
#else
72
1.48k
  random_globals_ctor(&random_globals);
73
1.48k
#endif
74
75
1.48k
  return SUCCESS;
76
1.48k
}
77
/* }}} */
78
79
/* {{{ */
80
PHP_MSHUTDOWN_FUNCTION(random)
81
0
{
82
0
#ifndef ZTS
83
0
  random_globals_dtor(&random_globals);
84
0
#endif
85
86
0
  return SUCCESS;
87
0
}
88
/* }}} */
89
90
/* {{{ php_random_bytes */
91
PHPAPI int php_random_bytes(void *bytes, size_t size, bool should_throw)
92
0
{
93
#ifdef PHP_WIN32
94
  /* Defer to CryptGenRandom on Windows */
95
  if (php_win32_get_random_bytes(bytes, size) == FAILURE) {
96
    if (should_throw) {
97
      zend_throw_exception(zend_ce_exception, "Could not gather sufficient random data", 0);
98
    }
99
    return FAILURE;
100
  }
101
#elif HAVE_COMMONCRYPTO_COMMONRANDOM_H
102
  /*
103
   * Purposely prioritized upon arc4random_buf for modern macOs releases
104
   * arc4random api on this platform uses `ccrng_generate` which returns
105
   * a status but silented to respect the "no fail" arc4random api interface
106
   * the vast majority of the time, it works fine ; but better make sure we catch failures
107
   */
108
  if (CCRandomGenerateBytes(bytes, size) != kCCSuccess) {
109
    if (should_throw) {
110
      zend_throw_exception(zend_ce_exception, "Error generating bytes", 0);
111
    }
112
    return FAILURE;
113
  }
114
#elif HAVE_DECL_ARC4RANDOM_BUF && ((defined(__OpenBSD__) && OpenBSD >= 201405) || (defined(__NetBSD__) && __NetBSD_Version__ >= 700000001) || defined(__APPLE__) || defined(__GLIBC__))
115
  arc4random_buf(bytes, size);
116
#else
117
0
  size_t read_bytes = 0;
118
0
  ssize_t n;
119
0
#if (defined(__linux__) && defined(SYS_getrandom)) || (defined(__FreeBSD__) && __FreeBSD_version >= 1200000) || (defined(__DragonFly__) && __DragonFly_version >= 500700) || defined(__sun)
120
  /* Linux getrandom(2) syscall or FreeBSD/DragonFlyBSD getrandom(2) function*/
121
  /* Keep reading until we get enough entropy */
122
0
  while (read_bytes < size) {
123
    /* Below, (bytes + read_bytes)  is pointer arithmetic.
124
125
       bytes   read_bytes  size
126
         |      |           |
127
        [#######=============] (we're going to write over the = region)
128
                 \\\\\\\\\\\\\
129
                  amount_to_read
130
131
    */
132
0
    size_t amount_to_read = size - read_bytes;
133
0
#if defined(__linux__)
134
0
    n = syscall(SYS_getrandom, bytes + read_bytes, amount_to_read, 0);
135
#else
136
    n = getrandom(bytes + read_bytes, amount_to_read, 0);
137
#endif
138
139
0
    if (n == -1) {
140
0
      if (errno == ENOSYS) {
141
        /* This can happen if PHP was compiled against a newer kernel where getrandom()
142
         * is available, but then runs on an older kernel without getrandom(). If this
143
         * happens we simply fall back to reading from /dev/urandom. */
144
0
        ZEND_ASSERT(read_bytes == 0);
145
0
        break;
146
0
      } else if (errno == EINTR || errno == EAGAIN) {
147
        /* Try again */
148
0
        continue;
149
0
      } else {
150
          /* If the syscall fails, fall back to reading from /dev/urandom */
151
0
        break;
152
0
      }
153
0
    }
154
155
#if __has_feature(memory_sanitizer)
156
    /* MSan does not instrument manual syscall invocations. */
157
    __msan_unpoison(bytes + read_bytes, n);
158
#endif
159
0
    read_bytes += (size_t) n;
160
0
  }
161
0
#endif
162
0
  if (read_bytes < size) {
163
0
    int    fd = RANDOM_G(fd);
164
0
    struct stat st;
165
166
0
    if (fd < 0) {
167
0
#ifdef HAVE_DEV_URANDOM
168
0
      fd = open("/dev/urandom", O_RDONLY);
169
0
#endif
170
0
      if (fd < 0) {
171
0
        if (should_throw) {
172
0
          zend_throw_exception(zend_ce_exception, "Cannot open source device", 0);
173
0
        }
174
0
        return FAILURE;
175
0
      }
176
      /* Does the file exist and is it a character device? */
177
0
      if (fstat(fd, &st) != 0 ||
178
# ifdef S_ISNAM
179
          !(S_ISNAM(st.st_mode) || S_ISCHR(st.st_mode))
180
# else
181
0
          !S_ISCHR(st.st_mode)
182
0
# endif
183
0
      ) {
184
0
        close(fd);
185
0
        if (should_throw) {
186
0
          zend_throw_exception(zend_ce_exception, "Error reading from source device", 0);
187
0
        }
188
0
        return FAILURE;
189
0
      }
190
0
      RANDOM_G(fd) = fd;
191
0
    }
192
193
0
    for (read_bytes = 0; read_bytes < size; read_bytes += (size_t) n) {
194
0
      n = read(fd, bytes + read_bytes, size - read_bytes);
195
0
      if (n <= 0) {
196
0
        break;
197
0
      }
198
0
    }
199
200
0
    if (read_bytes < size) {
201
0
      if (should_throw) {
202
0
        zend_throw_exception(zend_ce_exception, "Could not gather sufficient random data", 0);
203
0
      }
204
0
      return FAILURE;
205
0
    }
206
0
  }
207
0
#endif
208
209
0
  return SUCCESS;
210
0
}
211
/* }}} */
212
213
/* {{{ Return an arbitrary length of pseudo-random bytes as binary string */
214
PHP_FUNCTION(random_bytes)
215
0
{
216
0
  zend_long size;
217
0
  zend_string *bytes;
218
219
0
  ZEND_PARSE_PARAMETERS_START(1, 1)
220
0
    Z_PARAM_LONG(size)
221
0
  ZEND_PARSE_PARAMETERS_END();
222
223
0
  if (size < 1) {
224
0
    zend_argument_value_error(1, "must be greater than 0");
225
0
    RETURN_THROWS();
226
0
  }
227
228
0
  bytes = zend_string_alloc(size, 0);
229
230
0
  if (php_random_bytes_throw(ZSTR_VAL(bytes), size) == FAILURE) {
231
0
    zend_string_release_ex(bytes, 0);
232
0
    RETURN_THROWS();
233
0
  }
234
235
0
  ZSTR_VAL(bytes)[size] = '\0';
236
237
0
  RETURN_STR(bytes);
238
0
}
239
/* }}} */
240
241
/* {{{ */
242
PHPAPI int php_random_int(zend_long min, zend_long max, zend_long *result, bool should_throw)
243
0
{
244
0
  zend_ulong umax;
245
0
  zend_ulong trial;
246
247
0
  if (min == max) {
248
0
    *result = min;
249
0
    return SUCCESS;
250
0
  }
251
252
0
  umax = (zend_ulong) max - (zend_ulong) min;
253
254
0
  if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
255
0
    return FAILURE;
256
0
  }
257
258
  /* Special case where no modulus is required */
259
0
  if (umax == ZEND_ULONG_MAX) {
260
0
    *result = (zend_long)trial;
261
0
    return SUCCESS;
262
0
  }
263
264
  /* Increment the max so the range is inclusive of max */
265
0
  umax++;
266
267
  /* Powers of two are not biased */
268
0
  if ((umax & (umax - 1)) != 0) {
269
    /* Ceiling under which ZEND_LONG_MAX % max == 0 */
270
0
    zend_ulong limit = ZEND_ULONG_MAX - (ZEND_ULONG_MAX % umax) - 1;
271
272
    /* Discard numbers over the limit to avoid modulo bias */
273
0
    while (trial > limit) {
274
0
      if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
275
0
        return FAILURE;
276
0
      }
277
0
    }
278
0
  }
279
280
0
  *result = (zend_long)((trial % umax) + min);
281
0
  return SUCCESS;
282
0
}
283
/* }}} */
284
285
/* {{{ Return an arbitrary pseudo-random integer */
286
PHP_FUNCTION(random_int)
287
0
{
288
0
  zend_long min;
289
0
  zend_long max;
290
0
  zend_long result;
291
292
0
  ZEND_PARSE_PARAMETERS_START(2, 2)
293
0
    Z_PARAM_LONG(min)
294
0
    Z_PARAM_LONG(max)
295
0
  ZEND_PARSE_PARAMETERS_END();
296
297
0
  if (min > max) {
298
0
    zend_argument_value_error(1, "must be less than or equal to argument #2 ($max)");
299
0
    RETURN_THROWS();
300
0
  }
301
302
0
  if (php_random_int_throw(min, max, &result) == FAILURE) {
303
0
    RETURN_THROWS();
304
0
  }
305
306
0
  RETURN_LONG(result);
307
0
}
308
/* }}} */