Coverage Report

Created: 2025-06-13 06:43

/src/php-src/ext/random/csprng.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: Tim Düsterhus <timwolla@php.net>                            |
14
   |          Go Kudo <zeriyoshi@php.net>                                 |
15
   +----------------------------------------------------------------------+
16
*/
17
18
#ifdef HAVE_CONFIG_H
19
# include "config.h"
20
#endif
21
22
#include <stdlib.h>
23
#include <sys/stat.h>
24
#include <fcntl.h>
25
26
#include "php.h"
27
28
#include "Zend/zend_exceptions.h"
29
#include "Zend/zend_atomic.h"
30
31
#include "php_random.h"
32
#include "php_random_csprng.h"
33
34
#ifdef HAVE_UNISTD_H
35
# include <unistd.h>
36
#endif
37
38
#ifdef PHP_WIN32
39
# include "win32/time.h"
40
# include "win32/winutil.h"
41
# include <process.h>
42
#endif
43
44
#ifdef __linux__
45
# include <sys/syscall.h>
46
#endif
47
48
#ifdef HAVE_SYS_PARAM_H
49
# include <sys/param.h>
50
# if (defined(__FreeBSD__) && __FreeBSD_version > 1200000) || (defined(__DragonFly__) && __DragonFly_version >= 500700) || \
51
     (defined(__sun) && defined(HAVE_GETRANDOM)) || (defined(__NetBSD__) && __NetBSD_Version__ >= 1000000000) || defined(__midipix__)
52
#  include <sys/random.h>
53
# endif
54
#endif
55
56
#ifdef HAVE_COMMONCRYPTO_COMMONRANDOM_H
57
# include <CommonCrypto/CommonCryptoError.h>
58
# include <CommonCrypto/CommonRandom.h>
59
#endif
60
61
#if __has_feature(memory_sanitizer)
62
# include <sanitizer/msan_interface.h>
63
#endif
64
65
#ifndef PHP_WIN32
66
static zend_atomic_int random_fd = ZEND_ATOMIC_INT_INITIALIZER(-1);
67
#endif
68
69
ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_bytes_ex(void *bytes, size_t size, char *errstr, size_t errstr_size)
70
71
{
71
#ifdef PHP_WIN32
72
  /* Defer to CryptGenRandom on Windows */
73
  if (php_win32_get_random_bytes(bytes, size) == FAILURE) {
74
    snprintf(errstr, errstr_size, "Failed to retrieve randomness from the operating system (BCryptGenRandom)");
75
    return FAILURE;
76
  }
77
#elif defined(HAVE_COMMONCRYPTO_COMMONRANDOM_H)
78
  /*
79
   * Purposely prioritized upon arc4random_buf for modern macOs releases
80
   * arc4random api on this platform uses `ccrng_generate` which returns
81
   * a status but silented to respect the "no fail" arc4random api interface
82
   * the vast majority of the time, it works fine ; but better make sure we catch failures
83
   */
84
  if (CCRandomGenerateBytes(bytes, size) != kCCSuccess) {
85
    snprintf(errstr, errstr_size, "Failed to retrieve randomness from the operating system (CCRandomGenerateBytes)");
86
    return FAILURE;
87
  }
88
#elif defined(HAVE_ARC4RANDOM_BUF) && ((defined(__OpenBSD__) && OpenBSD >= 201405) || (defined(__NetBSD__) && __NetBSD_Version__ >= 700000001 && __NetBSD_Version__ < 1000000000) || \
89
  defined(__APPLE__) || defined(__HAIKU__))
90
  /*
91
   * OpenBSD until there is a valid equivalent
92
   * or NetBSD before the 10.x release
93
   * falls back to arc4random_buf
94
   * giving a decent output, the main benefit
95
   * is being (relatively) failsafe.
96
   * Older macOs releases fall also into this
97
   * category for reasons explained above.
98
   */
99
  arc4random_buf(bytes, size);
100
#else
101
71
  size_t read_bytes = 0;
102
71
# if (defined(__linux__) && defined(SYS_getrandom)) || (defined(__FreeBSD__) && __FreeBSD_version >= 1200000) || (defined(__DragonFly__) && __DragonFly_version >= 500700) || \
103
71
  (defined(__sun) && defined(HAVE_GETRANDOM)) || (defined(__NetBSD__) && __NetBSD_Version__ >= 1000000000) || defined(__midipix__)
104
  /* Linux getrandom(2) syscall or FreeBSD/DragonFlyBSD/NetBSD getrandom(2) function
105
   * Being a syscall, implemented in the kernel, getrandom offers higher quality output
106
   * compared to the arc4random api albeit a fallback to /dev/urandom is considered.
107
   */
108
142
  while (read_bytes < size) {
109
    /* Below, (bytes + read_bytes)  is pointer arithmetic.
110
111
       bytes   read_bytes  size
112
         |      |           |
113
        [#######=============] (we're going to write over the = region)
114
                 \\\\\\\\\\\\\
115
                  amount_to_read
116
    */
117
71
    size_t amount_to_read = size - read_bytes;
118
71
    ssize_t n;
119
120
71
    errno = 0;
121
71
#  if defined(__linux__)
122
71
    n = syscall(SYS_getrandom, bytes + read_bytes, amount_to_read, 0);
123
#  else
124
    n = getrandom(bytes + read_bytes, amount_to_read, 0);
125
#  endif
126
127
71
    if (n == -1) {
128
0
      if (errno == ENOSYS) {
129
        /* This can happen if PHP was compiled against a newer kernel where getrandom()
130
         * is available, but then runs on an older kernel without getrandom(). If this
131
         * happens we simply fall back to reading from /dev/urandom. */
132
0
        ZEND_ASSERT(read_bytes == 0);
133
0
        break;
134
0
      } else if (errno == EINTR || errno == EAGAIN) {
135
        /* Try again */
136
0
        continue;
137
0
      } else {
138
        /* If the syscall fails, fall back to reading from /dev/urandom */
139
0
        break;
140
0
      }
141
0
    }
142
143
#  if __has_feature(memory_sanitizer)
144
    /* MSan does not instrument manual syscall invocations. */
145
    __msan_unpoison(bytes + read_bytes, n);
146
#  endif
147
71
    read_bytes += (size_t) n;
148
71
  }
149
71
# endif
150
71
  if (read_bytes < size) {
151
0
    int    fd = zend_atomic_int_load_ex(&random_fd);
152
0
    struct stat st;
153
154
0
    if (fd < 0) {
155
0
      errno = 0;
156
0
      fd = open("/dev/urandom", O_RDONLY);
157
0
      if (fd < 0) {
158
0
        if (errno != 0) {
159
0
          snprintf(errstr, errstr_size, "Cannot open /dev/urandom: %s", strerror(errno));
160
0
        } else {
161
0
          snprintf(errstr, errstr_size, "Cannot open /dev/urandom");
162
0
        }
163
0
        return FAILURE;
164
0
      }
165
166
0
      errno = 0;
167
      /* Does the file exist and is it a character device? */
168
0
      if (fstat(fd, &st) != 0 ||
169
# ifdef S_ISNAM
170
          !(S_ISNAM(st.st_mode) || S_ISCHR(st.st_mode))
171
# else
172
0
          !S_ISCHR(st.st_mode)
173
0
# endif
174
0
      ) {
175
0
        close(fd);
176
0
        if (errno != 0) {
177
0
          snprintf(errstr, errstr_size, "Error reading from /dev/urandom: %s", strerror(errno));
178
0
        } else {
179
0
          snprintf(errstr, errstr_size, "Error reading from /dev/urandom");
180
0
        }
181
0
        return FAILURE;
182
0
      }
183
0
      int expected = -1;
184
0
      if (!zend_atomic_int_compare_exchange_ex(&random_fd, &expected, fd)) {
185
0
        close(fd);
186
        /* expected is now the actual value of random_fd */
187
0
        fd = expected;
188
0
      }
189
0
    }
190
191
0
    read_bytes = 0;
192
0
    while (read_bytes < size) {
193
0
      errno = 0;
194
0
      ssize_t n = read(fd, bytes + read_bytes, size - read_bytes);
195
196
0
      if (n <= 0) {
197
0
        if (errno != 0) {
198
0
          snprintf(errstr, errstr_size, "Could not gather sufficient random data: %s", strerror(errno));
199
0
        } else {
200
0
          snprintf(errstr, errstr_size, "Could not gather sufficient random data");
201
0
        }
202
0
        return FAILURE;
203
0
      }
204
205
0
      read_bytes += (size_t) n;
206
0
    }
207
0
  }
208
71
#endif
209
210
71
  return SUCCESS;
211
71
}
212
213
ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_bytes(void *bytes, size_t size, bool should_throw)
214
71
{
215
71
  char errstr[128];
216
71
  zend_result result = php_random_bytes_ex(bytes, size, errstr, sizeof(errstr));
217
218
71
  if (result == FAILURE && should_throw) {
219
0
    zend_throw_exception(random_ce_Random_RandomException, errstr, 0);
220
0
  }
221
222
71
  return result;
223
71
}
224
225
ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_int(zend_long min, zend_long max, zend_long *result, bool should_throw)
226
42
{
227
42
  zend_ulong umax;
228
42
  zend_ulong trial;
229
230
42
  if (min == max) {
231
5
    *result = min;
232
5
    return SUCCESS;
233
5
  }
234
235
37
  umax = (zend_ulong) max - (zend_ulong) min;
236
237
37
  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
37
  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
37
  umax++;
249
250
  /* Powers of two are not biased */
251
37
  if ((umax & (umax - 1)) != 0) {
252
    /* Ceiling under which ZEND_LONG_MAX % max == 0 */
253
12
    zend_ulong limit = ZEND_ULONG_MAX - (ZEND_ULONG_MAX % umax) - 1;
254
255
    /* Discard numbers over the limit to avoid modulo bias */
256
12
    while (trial > limit) {
257
0
      if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
258
0
        return FAILURE;
259
0
      }
260
0
    }
261
12
  }
262
263
37
  *result = (zend_long)((trial % umax) + min);
264
37
  return SUCCESS;
265
37
}
266
267
PHPAPI void php_random_csprng_shutdown(void)
268
0
{
269
0
#ifndef PHP_WIN32
270
0
  int fd = zend_atomic_int_exchange(&random_fd, -1);
271
0
  if (fd != -1) {
272
0
    close(fd);
273
0
  }
274
0
#endif
275
0
}