xref: /PHP-8.3/ext/random/csprng.c (revision f1e5c638)
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 
32 #if 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 #if HAVE_SYS_PARAM_H
47 # include <sys/param.h>
48 # if (__FreeBSD__ && __FreeBSD_version > 1200000) || (__DragonFly__ && __DragonFly_version >= 500700) || \
49      (defined(__sun) && defined(HAVE_GETRANDOM)) || (defined(__NetBSD__) && __NetBSD_Version__ >= 1000000000)
50 #  include <sys/random.h>
51 # endif
52 #endif
53 
54 #if 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 
php_random_bytes(void * bytes,size_t size,bool should_throw)63 PHPAPI zend_result php_random_bytes(void *bytes, size_t size, bool should_throw)
64 {
65 #ifdef PHP_WIN32
66 	/* Defer to CryptGenRandom on Windows */
67 	if (php_win32_get_random_bytes(bytes, size) == FAILURE) {
68 		if (should_throw) {
69 			zend_throw_exception(random_ce_Random_RandomException, "Failed to retrieve randomness from the operating system (BCryptGenRandom)", 0);
70 		}
71 		return FAILURE;
72 	}
73 #elif HAVE_COMMONCRYPTO_COMMONRANDOM_H
74 	/*
75 	 * Purposely prioritized upon arc4random_buf for modern macOs releases
76 	 * arc4random api on this platform uses `ccrng_generate` which returns
77 	 * a status but silented to respect the "no fail" arc4random api interface
78 	 * the vast majority of the time, it works fine ; but better make sure we catch failures
79 	 */
80 	if (CCRandomGenerateBytes(bytes, size) != kCCSuccess) {
81 		if (should_throw) {
82 			zend_throw_exception(random_ce_Random_RandomException, "Failed to retrieve randomness from the operating system (CCRandomGenerateBytes)", 0);
83 		}
84 		return FAILURE;
85 	}
86 #elif HAVE_DECL_ARC4RANDOM_BUF && ((defined(__OpenBSD__) && OpenBSD >= 201405) || (defined(__NetBSD__) && __NetBSD_Version__ >= 700000001 && __NetBSD_Version__ < 1000000000) || \
87   defined(__APPLE__))
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 	size_t read_bytes = 0;
100 # if (defined(__linux__) && defined(SYS_getrandom)) || (defined(__FreeBSD__) && __FreeBSD_version >= 1200000) || (defined(__DragonFly__) && __DragonFly_version >= 500700) || \
101   (defined(__sun) && defined(HAVE_GETRANDOM)) || (defined(__NetBSD__) && __NetBSD_Version__ >= 1000000000)
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 	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 		size_t amount_to_read = size - read_bytes;
116 		ssize_t n;
117 
118 		errno = 0;
119 #  if defined(__linux__)
120 		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 		if (n == -1) {
126 			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 				ZEND_ASSERT(read_bytes == 0);
131 				break;
132 			} else if (errno == EINTR || errno == EAGAIN) {
133 				/* Try again */
134 				continue;
135 			} else {
136 				/* If the syscall fails, fall back to reading from /dev/urandom */
137 				break;
138 			}
139 		}
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 		read_bytes += (size_t) n;
146 	}
147 # endif
148 	if (read_bytes < size) {
149 		int    fd = RANDOM_G(random_fd);
150 		struct stat st;
151 
152 		if (fd < 0) {
153 			errno = 0;
154 			fd = open("/dev/urandom", O_RDONLY);
155 			if (fd < 0) {
156 				if (should_throw) {
157 					if (errno != 0) {
158 						zend_throw_exception_ex(random_ce_Random_RandomException, 0, "Cannot open /dev/urandom: %s", strerror(errno));
159 					} else {
160 						zend_throw_exception_ex(random_ce_Random_RandomException, 0, "Cannot open /dev/urandom");
161 					}
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 (should_throw) {
177 					if (errno != 0) {
178 						zend_throw_exception_ex(random_ce_Random_RandomException, 0, "Error reading from /dev/urandom: %s", strerror(errno));
179 					} else {
180 						zend_throw_exception_ex(random_ce_Random_RandomException, 0, "Error reading from /dev/urandom");
181 					}
182 				}
183 				return FAILURE;
184 			}
185 			RANDOM_G(random_fd) = fd;
186 		}
187 
188 		read_bytes = 0;
189 		while (read_bytes < size) {
190 			errno = 0;
191 			ssize_t n = read(fd, bytes + read_bytes, size - read_bytes);
192 
193 			if (n <= 0) {
194 				if (should_throw) {
195 					if (errno != 0) {
196 						zend_throw_exception_ex(random_ce_Random_RandomException, 0, "Could not gather sufficient random data: %s", strerror(errno));
197 					} else {
198 						zend_throw_exception_ex(random_ce_Random_RandomException, 0, "Could not gather sufficient random data");
199 					}
200 				}
201 				return FAILURE;
202 			}
203 
204 			read_bytes += (size_t) n;
205 		}
206 	}
207 #endif
208 
209 	return SUCCESS;
210 }
211 
php_random_int(zend_long min,zend_long max,zend_long * result,bool should_throw)212 PHPAPI zend_result php_random_int(zend_long min, zend_long max, zend_long *result, bool should_throw)
213 {
214 	zend_ulong umax;
215 	zend_ulong trial;
216 
217 	if (min == max) {
218 		*result = min;
219 		return SUCCESS;
220 	}
221 
222 	umax = (zend_ulong) max - (zend_ulong) min;
223 
224 	if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
225 		return FAILURE;
226 	}
227 
228 	/* Special case where no modulus is required */
229 	if (umax == ZEND_ULONG_MAX) {
230 		*result = (zend_long)trial;
231 		return SUCCESS;
232 	}
233 
234 	/* Increment the max so the range is inclusive of max */
235 	umax++;
236 
237 	/* Powers of two are not biased */
238 	if ((umax & (umax - 1)) != 0) {
239 		/* Ceiling under which ZEND_LONG_MAX % max == 0 */
240 		zend_ulong limit = ZEND_ULONG_MAX - (ZEND_ULONG_MAX % umax) - 1;
241 
242 		/* Discard numbers over the limit to avoid modulo bias */
243 		while (trial > limit) {
244 			if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
245 				return FAILURE;
246 			}
247 		}
248 	}
249 
250 	*result = (zend_long)((trial % umax) + min);
251 	return SUCCESS;
252 }
253