Coverage Report

Created: 2022-02-19 20:27

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