@@ -85,6 +85,7 @@ def setUp(self):
8585 environment .start ()
8686 self .addCleanup (environment .stop )
8787 os .environ .pop ('V3_PACKAGE_PREREQUISITES' , None )
88+ os .environ .pop ('V3_PACKAGE_TOOLCHAIN' , None )
8889 self .tmp = tempfile .TemporaryDirectory ()
8990 self .addCleanup (self .tmp .cleanup )
9091 self .root = Path (self .tmp .name )
@@ -212,6 +213,84 @@ def test_selected_native_provider_only(self):
212213 self .assertTrue (self .violations (graph , tls = 'no' , surface = 'native' ))
213214 self .assertTrue (audit .graph_violations (graph , 'strict' , 'yes' , 'native' , self .policy , self .root / 'other-provider' ))
214215
216+ def test_selected_darwin_unwind_accepts_observed_llvm_closure_only (self ):
217+ prefix = Path ('/opt/homebrew/Cellar/llvm/23.1.0' )
218+ cxx = str (prefix / 'lib/c++/libc++.1.0.dylib' )
219+ abi = str (prefix / 'lib/c++/libc++abi.1.0.dylib' )
220+ unwind = str (prefix / 'lib/unwind/libunwind.1.0.dylib' )
221+ graph = {'nodes' : [{'id' : path } for path in ('consumer' , cxx , abi , unwind )],
222+ 'edges' : [{'parent' : parent , 'name' : child , 'child' : child } for parent , child in
223+ [('consumer' , cxx ), (cxx , abi ), (cxx , unwind ), (abi , unwind )]]}
224+ with patch .object (audit .platform , 'system' , return_value = 'Darwin' ):
225+ for selected in ('' , str (prefix .parent / '22.0.0' ), str (prefix )):
226+ with self .subTest (selected = selected ), patch .dict (os .environ , V3_PACKAGE_TOOLCHAIN = selected ):
227+ errors = self .violations (graph )
228+ if selected == str (prefix ):
229+ self .assertEqual ([], errors )
230+ else :
231+ self .assertEqual ([f'undeclared dependency (aggregate/strict): { parent } -> { unwind } '
232+ for parent in (cxx , abi )], errors )
233+
234+ def test_selected_darwin_unwind_requires_canonical_directory_and_numeric_name (self ):
235+ prefix = self .root / 'llvm/23.1.0'
236+ directory = prefix / 'lib/unwind'
237+ directory .mkdir (parents = True )
238+ target = directory / 'libunwind.1.0.dylib'
239+ target .write_text ('selected runtime' )
240+ alias = self .root / 'selected llvm'
241+ alias .symlink_to (prefix , target_is_directory = True )
242+ link = directory / 'libunwind.dylib'
243+ link .symlink_to (target .name )
244+ with patch .object (audit .platform , 'system' , return_value = 'Darwin' ), \
245+ patch .dict (os .environ , V3_PACKAGE_TOOLCHAIN = str (alias )):
246+ self .assertTrue (audit .runtime (str (target )))
247+ self .assertTrue (audit .runtime (str (link )))
248+ for path in (prefix / 'lib/libunwind.1.0.dylib' , prefix / 'lib/c++/libunwind.1.0.dylib' ,
249+ prefix .parent / 'other/lib/unwind/libunwind.1.0.dylib' ,
250+ directory / '../libunwind.1.0.dylib' , '/usr/lib/libunwind.1.dylib' ,
251+ directory / 'libunwind.bad.dylib' ,
252+ directory / 'libunwind.1.dylib.extra' , directory / 'libunwind.1..0.dylib' ,
253+ directory / 'libcurl.4.dylib' ):
254+ with self .subTest (path = str (path )):
255+ self .assertFalse (audit .runtime (str (path )))
256+ outside = self .root / 'libunwind.2.dylib'
257+ outside .touch ()
258+ escape = directory / 'libunwind.2.dylib'
259+ escape .symlink_to (outside )
260+ self .assertFalse (audit .runtime (str (escape )))
261+ target .unlink ()
262+ directory .rename (prefix / 'lib/sibling' )
263+ directory .symlink_to (prefix / 'lib/sibling' , target_is_directory = True )
264+ (directory / target .name ).touch ()
265+ self .assertFalse (audit .runtime (str (directory / target .name )))
266+
267+ def test_selected_unwind_still_inspects_hashes_and_rejects_transitive_provider (self ):
268+ prefix = self .root / 'llvm'
269+ directory = prefix / 'lib/unwind'
270+ directory .mkdir (parents = True )
271+ consumer , unwind , forbidden = self .root / 'consumer' , directory / 'libunwind.1.dylib' , self .root / 'libcrypto.dylib'
272+ links = {str (consumer ): [str (unwind )], str (unwind ): [str (forbidden )], str (forbidden ): []}
273+ for path in links :
274+ Path (path ).write_text ('inspected ' + path )
275+ inspected = []
276+ def inspect (path ):
277+ inspected .append (path )
278+ return [(child , child ) for child in links [path ]]
279+ with patch .object (audit .platform , 'system' , return_value = 'Darwin' ), \
280+ patch .dict (os .environ , V3_PACKAGE_TOOLCHAIN = str (prefix )):
281+ graph = audit .dependency_graph ([str (consumer )], inspect )
282+ self .assertEqual (set (links ), set (inspected ))
283+ self .assertEqual ({path : hashlib .sha256 (Path (path ).read_bytes ()).hexdigest () for path in links },
284+ {node ['id' ]: node ['sha256' ] for node in graph ['nodes' ]})
285+ self .assertEqual (['unselected native provider: ' + str (forbidden )], audit .graph_violations (
286+ graph , 'strict' , 'yes' , 'aggregate' , self .policy , self .root / 'provider' ))
287+ forbidden .rename (self .root / 'libmicrohttpd.dylib' )
288+ with self .assertRaisesRegex (audit .AuditError , 'unresolved dependency' ):
289+ audit .dependency_graph ([str (consumer )], inspect )
290+ with patch .object (audit .platform , 'system' , return_value = 'Linux' ), \
291+ patch .dict (os .environ , V3_PACKAGE_TOOLCHAIN = str (prefix )):
292+ self .assertFalse (audit .runtime (str (unwind )))
293+
215294 def test_unknown_system_library_is_not_allowed (self ):
216295 graph = {'nodes' : [{'id' : 'consumer' }, {'id' : '/usr/lib/libsurprise.dylib' }],
217296 'edges' : [{'parent' : 'consumer' , 'name' : '/usr/lib/libsurprise.dylib' , 'child' : '/usr/lib/libsurprise.dylib' }]}
0 commit comments