Coverage Report

Created: 2026-06-02 06:36

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