4242import sys
4343import tempfile
4444import textwrap
45+ import threading
4546import unittest
4647
4748
4849@unittest .skipUnless (sys .implementation .name == "graalpy" and sys .platform .startswith ("linux" ), "Linux GraalPy-specific test" )
4950class EntropySubprocessTests (unittest .TestCase ):
5051 HASH_SECRET_BYTES = 24
5152 RANDOM_SEED_BYTES = 624 * 4
52- HASH_AND_RANDOM_IMPORT_BYTES = HASH_SECRET_BYTES + RANDOM_SEED_BYTES
53+ RANDOM_INSTANCE_BYTES = HASH_SECRET_BYTES + RANDOM_SEED_BYTES
54+ RANDOM_MODULE_BYTES = HASH_SECRET_BYTES + (2 * RANDOM_SEED_BYTES )
55+ TEMPFILE_CANDIDATE_NAME_BYTES = HASH_SECRET_BYTES + (4 * RANDOM_SEED_BYTES )
5356 SSL_DATA_DIR = os .path .join (os .path .dirname (__file__ ), "ssldata" )
5457
55- def _run_with_init_device (self , data : bytes , code : str ):
56- with tempfile .NamedTemporaryFile (delete = False ) as f :
57- f .write (data )
58- path = f .name
59- try :
60- return self ._run_with_init_source (f"device:{ path } " , code )
61- finally :
62- os .unlink (path )
58+ def _run_with_init_pipe (self , byte_count : int , code : str ):
59+ with tempfile .TemporaryDirectory () as temp_dir :
60+ path = os .path .join (temp_dir , "initrandom" )
61+ subprocess .run (["mkfifo" , path ], check = True )
62+ writer_error = []
63+ bytes_written = 0
64+
65+ def feed_pipe ():
66+ nonlocal bytes_written
67+ try :
68+ fd = os .open (path , os .O_WRONLY )
69+ try :
70+ remaining = byte_count
71+ chunk = b"\x00 " * min (4096 , byte_count )
72+ while remaining > 0 :
73+ written = os .write (fd , chunk [:remaining ])
74+ bytes_written += written
75+ remaining -= written
76+ finally :
77+ os .close (fd )
78+ except BaseException as exc : # re-raised in the main thread
79+ writer_error .append (exc )
80+
81+ writer = threading .Thread (target = feed_pipe )
82+ writer .start ()
83+ try :
84+ result = self ._run_with_init_source (f"device:{ path } " , code )
85+ finally :
86+ writer .join (timeout = 10 )
87+ if writer .is_alive ():
88+ self .fail ("initrandom pipe writer thread did not finish" )
89+ if writer_error :
90+ raise writer_error [0 ]
91+ return result , bytes_written
6392
6493 def _run_with_init_source (self , source : str , code : str ):
6594 env = os .environ .copy ()
@@ -86,65 +115,64 @@ def assert_initrandom_exhausted(self, result):
86115 def assert_subprocess_ok (self , result ):
87116 self .assertEqual (0 , result .returncode , result )
88117
118+ def assert_initrandom_bytes_used (self , byte_count : int , code : str , stdout : str ):
119+ result , bytes_written = self ._run_with_init_pipe (byte_count , code )
120+ self .assert_subprocess_ok (result )
121+ self .assertEqual (byte_count , bytes_written )
122+ self .assertEqual (stdout , result .stdout .strip ())
123+
124+ exhausted_result , exhausted_written = self ._run_with_init_pipe (byte_count - 1 , code )
125+ self .assert_initrandom_exhausted (exhausted_result )
126+ self .assertEqual (byte_count - 1 , exhausted_written )
127+
89128 def test_startup_hash_secret_uses_initrandom (self ):
90- result = self ._run_with_init_device (b"\x00 " * 4 , "print('ok')" )
91- self .assert_initrandom_exhausted (result )
129+ self .assert_initrandom_bytes_used (self .HASH_SECRET_BYTES , "print('ok')" , "ok" )
92130
93131 def test__random_random_uses_initrandom (self ):
94- result = self ._run_with_init_device (
95- b" \x00 " * self .HASH_SECRET_BYTES ,
132+ self .assert_initrandom_bytes_used (
133+ self .RANDOM_INSTANCE_BYTES ,
96134 "import _random; _random.Random(); print('ok')" ,
135+ "ok" ,
97136 )
98- self .assert_initrandom_exhausted (result )
99137
100138 def test_random_module_import_uses_initrandom (self ):
101- result = self ._run_with_init_device (
102- b" \x00 " * self .HASH_SECRET_BYTES ,
139+ self .assert_initrandom_bytes_used (
140+ self .RANDOM_MODULE_BYTES ,
103141 "import random; print('ok')" ,
142+ "ok" ,
104143 )
105- self .assert_initrandom_exhausted (result )
106-
107- def test_systemrandom_does_not_mutate_random_state (self ):
108- result = self ._run_with_init_source (
109- "fixed:0x1234ABCD" ,
110- "import random; before = random.getstate(); "
111- "random.SystemRandom().getrandbits(32); "
112- "after = random.getstate(); "
113- "print(before == after)" ,
144+
145+ def test_systemrandom_does_not_use_additional_initrandom (self ):
146+ self .assert_initrandom_bytes_used (
147+ self .RANDOM_MODULE_BYTES ,
148+ "import random; random.SystemRandom().getrandbits(32); print('ok')" ,
149+ "ok" ,
114150 )
115- self .assert_subprocess_ok (result )
116- self .assertEqual ("True" , result .stdout .strip ())
117-
118- def test_secrets_does_not_mutate_random_state (self ):
119- result = self ._run_with_init_source (
120- "fixed:0x1234ABCD" ,
121- "import random; before = random.getstate(); "
122- "import secrets; secrets.token_hex(8); "
123- "after = random.getstate(); "
124- "print(before == after)" ,
151+
152+ def test_secrets_does_not_use_additional_initrandom (self ):
153+ self .assert_initrandom_bytes_used (
154+ self .RANDOM_MODULE_BYTES ,
155+ "import random; import secrets; secrets.token_hex(8); print('ok')" ,
156+ "ok" ,
125157 )
126- self .assert_subprocess_ok (result )
127- self .assertEqual ("True" , result .stdout .strip ())
128158
129159 def test_os_urandom_does_not_use_initrandom (self ):
130- result = self ._run_with_init_device (
131- b" \x00 " * self .HASH_SECRET_BYTES ,
160+ self .assert_initrandom_bytes_used (
161+ self .HASH_SECRET_BYTES ,
132162 "import os; print(len(os.urandom(16)))" ,
163+ "16" ,
133164 )
134- self .assert_subprocess_ok (result )
135- self .assertEqual ("16" , result .stdout .strip ())
136165
137166 def test_multiprocessing_process_import_does_not_use_initrandom (self ):
138- result = self ._run_with_init_device (
139- b" \x00 " * self .HASH_SECRET_BYTES ,
167+ self .assert_initrandom_bytes_used (
168+ self .HASH_SECRET_BYTES ,
140169 "import multiprocessing.process; print('ok')" ,
170+ "ok" ,
141171 )
142- self .assert_subprocess_ok (result )
143- self .assertEqual ("ok" , result .stdout .strip ())
144172
145- def test_multiprocessing_deliver_challenge_does_not_mutate_random_state (self ):
146- result = self ._run_with_init_source (
147- "fixed:0x1234ABCD" ,
173+ def test_multiprocessing_deliver_challenge_does_not_use_additional_initrandom (self ):
174+ self .assert_initrandom_bytes_used (
175+ self . RANDOM_MODULE_BYTES ,
148176 textwrap .dedent ("""
149177 import random
150178 import multiprocessing.connection as mc
@@ -161,48 +189,37 @@ def recv_bytes(self, _max):
161189 msg = self.sent[-1][len(mc._CHALLENGE):]
162190 return mc._create_response(auth, msg)
163191
164- before = random.getstate()
165192 d = Dummy()
166193 mc.deliver_challenge(d, auth)
167- after = random.getstate()
168- print(before == after)
194+ print('ok')
169195 """ ),
196+ "ok" ,
170197 )
171- self .assert_subprocess_ok (result )
172- self .assertEqual ("True" , result .stdout .strip ())
173198
174199 def test_tempfile_candidate_names_use_initrandom (self ):
175- result = self ._run_with_init_device (
176- b" \x00 " * self .HASH_AND_RANDOM_IMPORT_BYTES ,
200+ self .assert_initrandom_bytes_used (
201+ self .TEMPFILE_CANDIDATE_NAME_BYTES ,
177202 "import tempfile; next(tempfile._get_candidate_names()); print('ok')" ,
203+ "ok" ,
178204 )
179- self .assert_initrandom_exhausted (result )
180-
181- def test_tempfile_does_not_mutate_random_state (self ):
182- result = self ._run_with_init_source (
183- "fixed:0x1234ABCD" ,
184- "import random; before = random.getstate(); "
185- "import tempfile; next(tempfile._get_candidate_names()); "
186- "after = random.getstate(); "
187- "print(before == after)" ,
205+
206+ def test_tempfile_after_random_import_uses_initrandom (self ):
207+ self .assert_initrandom_bytes_used (
208+ self .TEMPFILE_CANDIDATE_NAME_BYTES ,
209+ "import random; import tempfile; next(tempfile._get_candidate_names()); print('ok')" ,
210+ "ok" ,
188211 )
189- self .assert_subprocess_ok (result )
190- self .assertEqual ("True" , result .stdout .strip ())
191-
192- def test_email_generator_boundary_mutates_random_state (self ):
193- result = self ._run_with_init_source (
194- "fixed:0x1234ABCD" ,
195- "import random; before = random.getstate(); "
196- "from email.generator import Generator; Generator._make_boundary(); "
197- "after = random.getstate(); "
198- "print(before == after)" ,
212+
213+ def test_email_generator_boundary_does_not_use_additional_initrandom (self ):
214+ self .assert_initrandom_bytes_used (
215+ self .RANDOM_MODULE_BYTES ,
216+ "import random; from email.generator import Generator; Generator._make_boundary(); print('ok')" ,
217+ "ok" ,
199218 )
200- self .assert_subprocess_ok (result )
201- self .assertEqual ("False" , result .stdout .strip ())
202219
203- def test_imaplib_connect_mutates_random_state (self ):
204- result = self ._run_with_init_source (
205- "fixed:0x1234ABCD" ,
220+ def test_imaplib_connect_does_not_use_additional_initrandom (self ):
221+ self .assert_initrandom_bytes_used (
222+ self . RANDOM_MODULE_BYTES ,
206223 textwrap .dedent ("""
207224 import random
208225 import imaplib
@@ -221,7 +238,6 @@ def _get_capabilities(self):
221238 def shutdown(self):
222239 pass
223240
224- before = random.getstate()
225241 d = Dummy.__new__(Dummy)
226242 d.debug = imaplib.Debug
227243 d.state = 'LOGOUT'
@@ -234,74 +250,62 @@ def shutdown(self):
234250 d._tls_established = False
235251 d._mode_ascii()
236252 d._connect()
237- after = random.getstate()
238- print(before == after)
253+ print('ok')
239254 """ ),
255+ "ok" ,
240256 )
241- self .assert_subprocess_ok (result )
242- self .assertEqual ("False" , result .stdout .strip ())
243257
244258 def test_pyexpat_import_does_not_use_additional_initrandom (self ):
245- result = self ._run_with_init_device (
246- b" \x00 " * self .HASH_SECRET_BYTES ,
259+ self .assert_initrandom_bytes_used (
260+ self .HASH_SECRET_BYTES ,
247261 "import pyexpat; print(pyexpat.__name__)" ,
262+ "pyexpat" ,
248263 )
249- self .assert_subprocess_ok (result )
250- self .assertEqual ("pyexpat" , result .stdout .strip ())
251264
252265 def test_pyexpat_parsercreate_does_not_use_additional_initrandom (self ):
253- result = self ._run_with_init_device (
254- b" \x00 " * self .HASH_SECRET_BYTES ,
266+ self .assert_initrandom_bytes_used (
267+ self .HASH_SECRET_BYTES ,
255268 "import pyexpat; p = pyexpat.ParserCreate(); print(type(p).__name__)" ,
269+ "xmlparser" ,
256270 )
257- self .assert_subprocess_ok (result )
258- self .assertEqual ("xmlparser" , result .stdout .strip ())
259271
260272 def test_sqlite3_import_does_not_use_additional_initrandom (self ):
261- result = self ._run_with_init_device (
262- b" \x00 " * self .HASH_SECRET_BYTES ,
273+ self .assert_initrandom_bytes_used (
274+ self .HASH_SECRET_BYTES ,
263275 "import _sqlite3; print(_sqlite3.__name__)" ,
276+ "_sqlite3" ,
264277 )
265- self .assert_subprocess_ok (result )
266- self .assertEqual ("_sqlite3" , result .stdout .strip ())
267278
268279 def test_sqlite3_randomblob_does_not_use_initrandom (self ):
269- result = self ._run_with_init_device (
270- b" \x00 " * self .HASH_SECRET_BYTES ,
280+ self .assert_initrandom_bytes_used (
281+ self .HASH_SECRET_BYTES ,
271282 "import sqlite3; "
272283 "conn = sqlite3.connect(':memory:'); "
273284 "print(conn.execute('select length(randomblob(16))').fetchone()[0])" ,
285+ "16" ,
274286 )
275- self .assert_subprocess_ok (result )
276- self .assertEqual ("16" , result .stdout .strip ())
277287
278288 def test_ssl_load_cert_chain_does_not_use_initrandom (self ):
279289 cert = os .path .join (self .SSL_DATA_DIR , "signed_cert.pem" )
280- result = self ._run_with_init_device (
281- b" \x00 " * self .HASH_SECRET_BYTES ,
290+ self .assert_initrandom_bytes_used (
291+ self .HASH_SECRET_BYTES ,
282292 f"import ssl; "
283293 f"ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER); "
284294 f"ctx.load_cert_chain(r'{ cert } '); "
285295 f"print('ok')" ,
296+ "ok" ,
286297 )
287- self .assert_subprocess_ok (result )
288- self .assertEqual ("ok" , result .stdout .strip ())
289-
290- def test_uuid1_mutates_random_state (self ):
291- result = self ._run_with_init_source (
292- "fixed:0x1234ABCD" ,
293- "import random; before = random.getstate(); "
294- "import uuid; uuid.uuid1(node=1); "
295- "after = random.getstate(); "
296- "print(before == after)" ,
298+
299+ def test_uuid1_does_not_use_additional_initrandom (self ):
300+ self .assert_initrandom_bytes_used (
301+ self .RANDOM_MODULE_BYTES ,
302+ "import random; import uuid; uuid.uuid1(node=1); print('ok')" ,
303+ "ok" ,
297304 )
298- self .assert_subprocess_ok (result )
299- self .assertEqual ("False" , result .stdout .strip ())
300305
301306 def test_uuid4_does_not_use_initrandom (self ):
302- result = self ._run_with_init_device (
303- b" \x00 " * self .HASH_SECRET_BYTES ,
307+ self .assert_initrandom_bytes_used (
308+ self .HASH_SECRET_BYTES ,
304309 "import uuid; print(uuid.uuid4().version)" ,
310+ "4" ,
305311 )
306- self .assert_subprocess_ok (result )
307- self .assertEqual ("4" , result .stdout .strip ())
0 commit comments