Skip to content

Commit b14dd85

Browse files
authored
random: Move the CSPRNG implementation into a separate C file (#10668)
The CSPRNG is a delicate and security relevant piece of code and having it in the giant random.c makes it much harder to verify changes to it. Split it into a separate file.
1 parent d5c649b commit b14dd85

File tree

4 files changed

+254
-213
lines changed

4 files changed

+254
-213
lines changed

ext/random/config.m4

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dnl Setup extension
1919
dnl
2020
PHP_NEW_EXTENSION(random,
2121
random.c \
22+
csprng.c \
2223
engine_combinedlcg.c \
2324
engine_mt19937.c \
2425
engine_pcgoneseq128xslrr64.c \

ext/random/config.w32

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
EXTENSION("random", "random.c", false /* never shared */, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1");
22
PHP_RANDOM="yes";
3-
ADD_SOURCES(configure_module_dirname, "engine_combinedlcg.c engine_mt19937.c engine_pcgoneseq128xslrr64.c engine_xoshiro256starstar.c engine_secure.c engine_user.c gammasection.c randomizer.c", "random");
3+
ADD_SOURCES(configure_module_dirname, "csprng.c engine_combinedlcg.c engine_mt19937.c engine_pcgoneseq128xslrr64.c engine_xoshiro256starstar.c engine_secure.c engine_user.c gammasection.c randomizer.c", "random");
44
PHP_INSTALL_HEADERS("ext/random", "php_random.h");

ext/random/csprng.c

+252
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
| [email protected] so we can mail you a copy immediately. |
12+
+----------------------------------------------------------------------+
13+
| Authors: Tim Düsterhus <[email protected]> |
14+
| Go Kudo <[email protected]> |
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(__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+
63+
PHPAPI int 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(__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+
212+
PHPAPI int 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+
}

0 commit comments

Comments
 (0)