xref: /php-src/ext/random/csprng.c (revision d00dd2b4)
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 
php_random_bytes_ex(void * bytes,size_t size,char * errstr,size_t errstr_size)69 ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_bytes_ex(void *bytes, size_t size, char *errstr, size_t errstr_size)
70 {
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 	size_t read_bytes = 0;
102 # if (defined(__linux__) && defined(SYS_getrandom)) || (defined(__FreeBSD__) && __FreeBSD_version >= 1200000) || (defined(__DragonFly__) && __DragonFly_version >= 500700) || \
103   (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 	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 		size_t amount_to_read = size - read_bytes;
118 		ssize_t n;
119 
120 		errno = 0;
121 #  if defined(__linux__)
122 		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 		if (n == -1) {
128 			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 				ZEND_ASSERT(read_bytes == 0);
133 				break;
134 			} else if (errno == EINTR || errno == EAGAIN) {
135 				/* Try again */
136 				continue;
137 			} else {
138 				/* If the syscall fails, fall back to reading from /dev/urandom */
139 				break;
140 			}
141 		}
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 		read_bytes += (size_t) n;
148 	}
149 # endif
150 	if (read_bytes < size) {
151 		int    fd = zend_atomic_int_load_ex(&random_fd);
152 		struct stat st;
153 
154 		if (fd < 0) {
155 			errno = 0;
156 			fd = open("/dev/urandom", O_RDONLY);
157 			if (fd < 0) {
158 				if (errno != 0) {
159 					snprintf(errstr, errstr_size, "Cannot open /dev/urandom: %s", strerror(errno));
160 				} else {
161 					snprintf(errstr, errstr_size, "Cannot open /dev/urandom");
162 				}
163 				return FAILURE;
164 			}
165 
166 			errno = 0;
167 			/* Does the file exist and is it a character device? */
168 			if (fstat(fd, &st) != 0 ||
169 # ifdef S_ISNAM
170 					!(S_ISNAM(st.st_mode) || S_ISCHR(st.st_mode))
171 # else
172 					!S_ISCHR(st.st_mode)
173 # endif
174 			) {
175 				close(fd);
176 				if (errno != 0) {
177 					snprintf(errstr, errstr_size, "Error reading from /dev/urandom: %s", strerror(errno));
178 				} else {
179 					snprintf(errstr, errstr_size, "Error reading from /dev/urandom");
180 				}
181 				return FAILURE;
182 			}
183 			int expected = -1;
184 			if (!zend_atomic_int_compare_exchange_ex(&random_fd, &expected, fd)) {
185 				close(fd);
186 				/* expected is now the actual value of random_fd */
187 				fd = expected;
188 			}
189 		}
190 
191 		read_bytes = 0;
192 		while (read_bytes < size) {
193 			errno = 0;
194 			ssize_t n = read(fd, bytes + read_bytes, size - read_bytes);
195 
196 			if (n <= 0) {
197 				if (errno != 0) {
198 					snprintf(errstr, errstr_size, "Could not gather sufficient random data: %s", strerror(errno));
199 				} else {
200 					snprintf(errstr, errstr_size, "Could not gather sufficient random data");
201 				}
202 				return FAILURE;
203 			}
204 
205 			read_bytes += (size_t) n;
206 		}
207 	}
208 #endif
209 
210 	return SUCCESS;
211 }
212 
php_random_bytes(void * bytes,size_t size,bool should_throw)213 ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_bytes(void *bytes, size_t size, bool should_throw)
214 {
215 	char errstr[128];
216 	zend_result result = php_random_bytes_ex(bytes, size, errstr, sizeof(errstr));
217 
218 	if (result == FAILURE && should_throw) {
219 		zend_throw_exception(random_ce_Random_RandomException, errstr, 0);
220 	}
221 
222 	return result;
223 }
224 
php_random_int(zend_long min,zend_long max,zend_long * result,bool should_throw)225 ZEND_ATTRIBUTE_NONNULL PHPAPI zend_result php_random_int(zend_long min, zend_long max, zend_long *result, bool should_throw)
226 {
227 	zend_ulong umax;
228 	zend_ulong trial;
229 
230 	if (min == max) {
231 		*result = min;
232 		return SUCCESS;
233 	}
234 
235 	umax = (zend_ulong) max - (zend_ulong) min;
236 
237 	if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
238 		return FAILURE;
239 	}
240 
241 	/* Special case where no modulus is required */
242 	if (umax == ZEND_ULONG_MAX) {
243 		*result = (zend_long)trial;
244 		return SUCCESS;
245 	}
246 
247 	/* Increment the max so the range is inclusive of max */
248 	umax++;
249 
250 	/* Powers of two are not biased */
251 	if ((umax & (umax - 1)) != 0) {
252 		/* Ceiling under which ZEND_LONG_MAX % max == 0 */
253 		zend_ulong limit = ZEND_ULONG_MAX - (ZEND_ULONG_MAX % umax) - 1;
254 
255 		/* Discard numbers over the limit to avoid modulo bias */
256 		while (trial > limit) {
257 			if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
258 				return FAILURE;
259 			}
260 		}
261 	}
262 
263 	*result = (zend_long)((trial % umax) + min);
264 	return SUCCESS;
265 }
266 
php_random_csprng_shutdown(void)267 PHPAPI void php_random_csprng_shutdown(void)
268 {
269 #ifndef PHP_WIN32
270 	int fd = zend_atomic_int_exchange(&random_fd, -1);
271 	if (fd != -1) {
272 		close(fd);
273 	}
274 #endif
275 }
276