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