xref: /php-src/ext/random/csprng.c (revision ff76cb73)
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