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