xref: /PHP-8.0/ext/standard/random.c (revision d830a1f6)
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    | http://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: Sammy Kaye Powers <me@sammyk.me>                            |
14    +----------------------------------------------------------------------+
15 */
16 
17 #include <stdlib.h>
18 #include <sys/stat.h>
19 #include <fcntl.h>
20 #include <math.h>
21 
22 #include "php.h"
23 #include "zend_exceptions.h"
24 #include "php_random.h"
25 
26 #ifdef PHP_WIN32
27 # include "win32/winutil.h"
28 #endif
29 #ifdef __linux__
30 # include <sys/syscall.h>
31 #endif
32 #if defined(__OpenBSD__) || defined(__NetBSD__) || defined(__FreeBSD__)
33 # include <sys/param.h>
34 # if __FreeBSD__ && __FreeBSD_version > 1200000
35 #  include <sys/random.h>
36 # endif
37 #endif
38 #if HAVE_COMMONCRYPTO_COMMONRANDOM_H
39 # include <CommonCrypto/CommonCryptoError.h>
40 # include <CommonCrypto/CommonRandom.h>
41 #endif
42 
43 #if __has_feature(memory_sanitizer)
44 # include <sanitizer/msan_interface.h>
45 #endif
46 
47 #ifdef ZTS
48 int random_globals_id;
49 #else
50 php_random_globals random_globals;
51 #endif
52 
random_globals_ctor(php_random_globals * random_globals_p)53 static void random_globals_ctor(php_random_globals *random_globals_p)
54 {
55 	random_globals_p->fd = -1;
56 }
57 
random_globals_dtor(php_random_globals * random_globals_p)58 static void random_globals_dtor(php_random_globals *random_globals_p)
59 {
60 	if (random_globals_p->fd > 0) {
61 		close(random_globals_p->fd);
62 		random_globals_p->fd = -1;
63 	}
64 }
65 
66 /* {{{ */
PHP_MINIT_FUNCTION(random)67 PHP_MINIT_FUNCTION(random)
68 {
69 #ifdef ZTS
70 	ts_allocate_id(&random_globals_id, sizeof(php_random_globals), (ts_allocate_ctor)random_globals_ctor, (ts_allocate_dtor)random_globals_dtor);
71 #else
72 	random_globals_ctor(&random_globals);
73 #endif
74 
75 	return SUCCESS;
76 }
77 /* }}} */
78 
79 /* {{{ */
PHP_MSHUTDOWN_FUNCTION(random)80 PHP_MSHUTDOWN_FUNCTION(random)
81 {
82 #ifndef ZTS
83 	random_globals_dtor(&random_globals);
84 #endif
85 
86 	return SUCCESS;
87 }
88 /* }}} */
89 
90 /* {{{ php_random_bytes */
php_random_bytes(void * bytes,size_t size,zend_bool should_throw)91 PHPAPI int php_random_bytes(void *bytes, size_t size, zend_bool should_throw)
92 {
93 #ifdef PHP_WIN32
94 	/* Defer to CryptGenRandom on Windows */
95 	if (php_win32_get_random_bytes(bytes, size) == FAILURE) {
96 		if (should_throw) {
97 			zend_throw_exception(zend_ce_exception, "Could not gather sufficient random data", 0);
98 		}
99 		return FAILURE;
100 	}
101 #elif HAVE_COMMONCRYPTO_COMMONRANDOM_H
102 	/*
103 	 * Purposely prioritized upon arc4random_buf for modern macOs releases
104 	 * arc4random api on this platform uses `ccrng_generate` which returns
105 	 * a status but silented to respect the "no fail" arc4random api interface
106 	 * the vast majority of the time, it works fine ; but better make sure we catch failures
107 	 */
108 	if (CCRandomGenerateBytes(bytes, size) != kCCSuccess) {
109 		if (should_throw) {
110 			zend_throw_exception(zend_ce_exception, "Error generating bytes", 0);
111 		}
112 		return FAILURE;
113 	}
114 #elif HAVE_DECL_ARC4RANDOM_BUF && ((defined(__OpenBSD__) && OpenBSD >= 201405) || (defined(__NetBSD__) && __NetBSD_Version__ >= 700000001) || defined(__APPLE__))
115 	arc4random_buf(bytes, size);
116 #else
117 	size_t read_bytes = 0;
118 	ssize_t n;
119 #if (defined(__linux__) && defined(SYS_getrandom)) || (defined(__FreeBSD__) && __FreeBSD_version >= 1200000)
120 	/* Linux getrandom(2) syscall or FreeBSD getrandom(2) function*/
121 	/* Keep reading until we get enough entropy */
122 	while (read_bytes < size) {
123 		/* Below, (bytes + read_bytes)  is pointer arithmetic.
124 
125 		   bytes   read_bytes  size
126 		     |      |           |
127 		    [#######=============] (we're going to write over the = region)
128 		             \\\\\\\\\\\\\
129 		              amount_to_read
130 
131 		*/
132 		size_t amount_to_read = size - read_bytes;
133 #if defined(__linux__)
134 		n = syscall(SYS_getrandom, bytes + read_bytes, amount_to_read, 0);
135 #else
136 		n = getrandom(bytes + read_bytes, amount_to_read, 0);
137 #endif
138 
139 		if (n == -1) {
140 			if (errno == ENOSYS) {
141 				/* This can happen if PHP was compiled against a newer kernel where getrandom()
142 				 * is available, but then runs on an older kernel without getrandom(). If this
143 				 * happens we simply fall back to reading from /dev/urandom. */
144 				ZEND_ASSERT(read_bytes == 0);
145 				break;
146 			} else if (errno == EINTR || errno == EAGAIN) {
147 				/* Try again */
148 				continue;
149 			} else {
150 			    /* If the syscall fails, fall back to reading from /dev/urandom */
151 				break;
152 			}
153 		}
154 
155 #if __has_feature(memory_sanitizer)
156 		/* MSan does not instrument manual syscall invocations. */
157 		__msan_unpoison(bytes + read_bytes, n);
158 #endif
159 		read_bytes += (size_t) n;
160 	}
161 #endif
162 	if (read_bytes < size) {
163 		int    fd = RANDOM_G(fd);
164 		struct stat st;
165 
166 		if (fd < 0) {
167 #if HAVE_DEV_URANDOM
168 			fd = open("/dev/urandom", O_RDONLY);
169 #endif
170 			if (fd < 0) {
171 				if (should_throw) {
172 					zend_throw_exception(zend_ce_exception, "Cannot open source device", 0);
173 				}
174 				return FAILURE;
175 			}
176 			/* Does the file exist and is it a character device? */
177 			if (fstat(fd, &st) != 0 ||
178 # ifdef S_ISNAM
179 					!(S_ISNAM(st.st_mode) || S_ISCHR(st.st_mode))
180 # else
181 					!S_ISCHR(st.st_mode)
182 # endif
183 			) {
184 				close(fd);
185 				if (should_throw) {
186 					zend_throw_exception(zend_ce_exception, "Error reading from source device", 0);
187 				}
188 				return FAILURE;
189 			}
190 			RANDOM_G(fd) = fd;
191 		}
192 
193 		for (read_bytes = 0; read_bytes < size; read_bytes += (size_t) n) {
194 			n = read(fd, bytes + read_bytes, size - read_bytes);
195 			if (n <= 0) {
196 				break;
197 			}
198 		}
199 
200 		if (read_bytes < size) {
201 			if (should_throw) {
202 				zend_throw_exception(zend_ce_exception, "Could not gather sufficient random data", 0);
203 			}
204 			return FAILURE;
205 		}
206 	}
207 #endif
208 
209 	return SUCCESS;
210 }
211 /* }}} */
212 
213 /* {{{ Return an arbitrary length of pseudo-random bytes as binary string */
PHP_FUNCTION(random_bytes)214 PHP_FUNCTION(random_bytes)
215 {
216 	zend_long size;
217 	zend_string *bytes;
218 
219 	ZEND_PARSE_PARAMETERS_START(1, 1)
220 		Z_PARAM_LONG(size)
221 	ZEND_PARSE_PARAMETERS_END();
222 
223 	if (size < 1) {
224 		zend_argument_value_error(1, "must be greater than 0");
225 		RETURN_THROWS();
226 	}
227 
228 	bytes = zend_string_alloc(size, 0);
229 
230 	if (php_random_bytes_throw(ZSTR_VAL(bytes), size) == FAILURE) {
231 		zend_string_release_ex(bytes, 0);
232 		RETURN_THROWS();
233 	}
234 
235 	ZSTR_VAL(bytes)[size] = '\0';
236 
237 	RETURN_STR(bytes);
238 }
239 /* }}} */
240 
241 /* {{{ */
php_random_int(zend_long min,zend_long max,zend_long * result,zend_bool should_throw)242 PHPAPI int php_random_int(zend_long min, zend_long max, zend_long *result, zend_bool should_throw)
243 {
244 	zend_ulong umax;
245 	zend_ulong trial;
246 
247 	if (min == max) {
248 		*result = min;
249 		return SUCCESS;
250 	}
251 
252 	umax = (zend_ulong) max - (zend_ulong) min;
253 
254 	if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
255 		return FAILURE;
256 	}
257 
258 	/* Special case where no modulus is required */
259 	if (umax == ZEND_ULONG_MAX) {
260 		*result = (zend_long)trial;
261 		return SUCCESS;
262 	}
263 
264 	/* Increment the max so the range is inclusive of max */
265 	umax++;
266 
267 	/* Powers of two are not biased */
268 	if ((umax & (umax - 1)) != 0) {
269 		/* Ceiling under which ZEND_LONG_MAX % max == 0 */
270 		zend_ulong limit = ZEND_ULONG_MAX - (ZEND_ULONG_MAX % umax) - 1;
271 
272 		/* Discard numbers over the limit to avoid modulo bias */
273 		while (trial > limit) {
274 			if (php_random_bytes(&trial, sizeof(trial), should_throw) == FAILURE) {
275 				return FAILURE;
276 			}
277 		}
278 	}
279 
280 	*result = (zend_long)((trial % umax) + min);
281 	return SUCCESS;
282 }
283 /* }}} */
284 
285 /* {{{ Return an arbitrary pseudo-random integer */
PHP_FUNCTION(random_int)286 PHP_FUNCTION(random_int)
287 {
288 	zend_long min;
289 	zend_long max;
290 	zend_long result;
291 
292 	ZEND_PARSE_PARAMETERS_START(2, 2)
293 		Z_PARAM_LONG(min)
294 		Z_PARAM_LONG(max)
295 	ZEND_PARSE_PARAMETERS_END();
296 
297 	if (min > max) {
298 		zend_argument_value_error(1, "must be less than or equal to argument #2 ($max)");
299 		RETURN_THROWS();
300 	}
301 
302 	if (php_random_int_throw(min, max, &result) == FAILURE) {
303 		RETURN_THROWS();
304 	}
305 
306 	RETURN_LONG(result);
307 }
308 /* }}} */
309