char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1use std:: fs; 2use std:: path:: Path ; 3use std:: process::{ Command , Output }; 4use std:: sync:: Arc ; 5 6use avril_runtime:: caps::{ Capabilities , Category , Initial , NetGrant , PathTarget }; 7 8fn policy ( zero : bool , grants : & [( Category , bool , & str )], cwd : & Path ) ->Arc < Capabilities > { 9let mut initial =Initial :: new ( zero); 10for ( category, deny, value) in grants{ 11 initial. grant ( * category, * deny, value, cwd). unwrap (); 12} 13Capabilities :: new ( initial, cwd. to_owned ()) 14} 15 16fn run ( dir : & Path , args : & [ & str ]) ->Output { 17Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 18. arg ( "run" ) 19. current_dir ( dir) 20. args ( args) 21. output () 22. unwrap () 23} 24 25# [ test ] 26fn network_grants_are_exact_normalized_and_port_scoped () { 27let dir = tempfile:: tempdir (). unwrap (); 28let caps =policy ( 29true , 30& [ 31( Category :: Net , false , "BÜCHER.example" ), 32( Category :: Net , false , "localhost:80" ), 33( Category :: Net , false , "[2001:0db8::1]:443" ), 34( Category :: Net , false , "[::2]" ), 35( Category :: Net , false , "0x7f.1" ), 36( Category :: Net , true , "bücher.example:443" ), 37], 38 dir. path (), 39); 40assert! ( caps. check_net ( "xn--bcher-kva.example." , 8080 ). is_ok ()); 41assert! ( caps. check_net ( "bücher.example。" , 8080 ). is_ok ()); 42assert! ( caps. check_net ( "bücher.example" , 443 ). is_err ()); 43assert! ( caps. check_net ( "sub.xn--bcher-kva.example" , 8080 ). is_err ()); 44assert! ( caps. check_net ( "localhost" , 80 ). is_ok ()); 45assert! ( caps. check_net ( "localhost" , 443 ). is_err ()); 46assert! ( caps. check_net ( "[2001:db8::1]" , 443 ). is_ok ()); 47assert! ( caps. check_net ( "[2001:db8::1]" , 80 ). is_err ()); 48assert! ( caps. check_net ( "2001:db8::1" , 443 ). is_ok ()); 49assert! ( caps. check_net ( "2001:0db8::1" , 443 ). is_ok ()); 50assert! ( caps. check_net ( "2001:db8::1" , 80 ). is_err ()); 51assert! ( caps. check_net ( "2001:db8::2" , 443 ). is_err ()); 52assert! ( caps. check_net ( "[2001:db8::1]:443" , 443 ). is_err ()); 53assert! ( caps. check_net ( "::2" , 80 ). is_ok ()); 54assert! ( caps. check_net ( "::2" , 443 ). is_ok ()); 55assert! ( caps. check_net ( "127.0.0.1" , 1234 ). is_ok ()); 56for grantin [ 57"" , 58"a,b" , 59"https://a" , 60"user@a" , 61"a/path" , 62"a?b" , 63"a#b" , 64"a:" , 65"a:65536" , 66"a:1x" , 67"a:-1" , 68"::1" , 69"[::1]extra" , 70"[::1]extra:80" , 71"[::1]:" , 72"[::1]:80]" , 73"a b" , 74"*.example.com" , 75"a%2eb" , 76] { 77assert! ( NetGrant :: parse ( grant). is_err (), "{grant}" ); 78} 79} 80 81# [ test ] 82fn pledges_are_atomic_monotonic_and_inherited_as_snapshots () { 83let dir = tempfile:: tempdir (). unwrap (); 84let caps =policy ( false , & [], dir. path ()); 85assert! ( 86 caps. pledge ( vec! [ 87( Category :: Net , Some ( vec! []), vec! []), 88( Category :: Read , None , vec! [ "\0" . into ()]) 89]) 90. is_err () 91); 92assert! ( caps. check_net ( "a.example" , 80 ). is_ok ()); 93let child =Capabilities :: inherit ( caps. clone ()); 94 child 95. pledge ( vec! [( 96Category :: Net , 97Some ( vec! [ "a.example" . into (), "b.example" . into ()]), 98vec! [], 99)]) 100. unwrap (); 101assert! ( child. check_net ( "a.example" , 80 ). is_ok ()); 102assert! ( child. check_net ( "c.example" , 80 ). is_err ()); 103 caps. pledge ( vec! [( Category :: Net , None , vec! [ "a.example:80" . into ()])]) 104. unwrap (); 105assert! ( caps. check_net ( "a.example" , 80 ). is_err ()); 106assert! ( child. check_net ( "a.example" , 80 ). is_ok ()); 107assert! ( child. check_net ( "a.example" , 443 ). is_ok ()); 108 child 109. pledge ( vec! [( 110Category :: Net , 111Some ( vec! [ "b.example" . into (), "c.example" . into ()]), 112vec! [], 113)]) 114. unwrap (); 115assert! ( child. check_net ( "a.example" , 443 ). is_err ()); 116assert! ( child. check_net ( "b.example" , 443 ). is_ok ()); 117assert! ( child. check_net ( "c.example" , 443 ). is_err ()); 118 caps. pledge ( vec! [( Category :: Net , Some ( vec! []), vec! [])]) 119. unwrap (); 120assert! ( caps. check_net ( "b.example" , 443 ). is_err ()); 121assert! ( child. check_net ( "b.example" , 443 ). is_ok ()); 122let later =Capabilities :: inherit ( caps. clone ()); 123assert! ( later. check_net ( "b.example" , 443 ). is_err ()); 124assert! ( 125policy ( true , & [], dir. path ()) 126. check_net ( "b.example" , 443 ) 127. is_err () 128); 129} 130 131# [ test ] 132fn read_implies_module_loading_but_execution_denials_override_it () { 133let dir = tempfile:: tempdir (). unwrap (); 134 fs:: create_dir ( dir. path (). join ( "app" )). unwrap (); 135 fs:: create_dir ( dir. path (). join ( "application" )). unwrap (); 136for filein [ "app/public.js" , "app/private.js" , "application/other.js" ] { 137 fs:: write ( dir. path (). join ( file), "" ). unwrap (); 138} 139let public =PathTarget :: resolve ( dir. path (). join ( "app/public.js" )). unwrap (); 140let private =PathTarget :: resolve ( dir. path (). join ( "app/private.js" )). unwrap (); 141let outside =PathTarget :: resolve ( dir. path (). join ( "application/other.js" )). unwrap (); 142let caps =policy ( 143true , 144& [ 145( Category :: Read , false , "app" ), 146( Category :: JsExec , true , "app/private.js" ), 147], 148 dir. path (), 149); 150assert! ( caps. check_read ( & private). is_ok ()); 151assert! ( caps. check_js_exec ( & private). is_err ()); 152assert! ( caps. check_js_exec ( & public). is_ok ()); 153assert! ( caps. check_js_exec ( & outside). is_err ()); 154 caps. pledge ( vec! [( 155Category :: JsExec , 156Some ( vec! [ "application" . into ()]), 157vec! [], 158)]) 159. unwrap (); 160assert! ( caps. check_js_exec ( & public). is_err ()); 161let exec_only =policy ( true , & [( Category :: JsExec , false , "app" )], dir. path ()); 162assert! ( exec_only. check_js_exec ( & public). is_ok ()); 163assert! ( exec_only. check_read ( & public). is_err ()); 164let inherited_read =policy ( true , & [( Category :: Read , false , "app" )], dir. path ()); 165 inherited_read 166. pledge ( vec! [( Category :: Read , Some ( vec! []), vec! [])]) 167. unwrap (); 168assert! ( inherited_read. check_js_exec ( & public). is_err ()); 169} 170 171# [ test ] 172fn zero_keeps_stdio_and_the_entry_but_not_its_directory () { 173let dir = tempfile:: tempdir (). unwrap (); 174 fs:: write ( 175 dir. path (). join ( "main.js" ), 176r#" 177console.log('stdio'); 178for (const path of ['./dep.js', '././dep.js']) { 179try { await import(path); throw new Error('Unexpected import'); } 180catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } 181} 182export const main = import.meta.main; 183import(import.meta.url).then(namespace => console.log(namespace.main)); 184"# , 185) 186. unwrap (); 187 fs:: write ( 188 dir. path (). join ( "dep.js" ), 189"throw new Error('Executed denied module');" , 190) 191. unwrap (); 192let output =run ( dir. path (), & [ "-0" , "main.js" ]); 193assert! ( 194 output. status . success (), 195"{}" , 196String :: from_utf8_lossy ( & output. stderr ) 197); 198assert_eq! ( output. stdout , b"stdio\ntrue\n" ); 199} 200 201# [ test ] 202fn repeated_cli_grants_preserve_commas_and_apply_denials_last () { 203let dir = tempfile:: tempdir (). unwrap (); 204for folderin [ "app, data" , "other" , "app, data/private" ] { 205 fs:: create_dir_all ( dir. path (). join ( folder)). unwrap (); 206} 207 fs:: write ( 208 dir. path (). join ( "app, data/one.ts" ), 209"export const one: number = 1;" , 210) 211. unwrap (); 212 fs:: write ( dir. path (). join ( "other/two.js" ), "export const two = 2;" ). unwrap (); 213 fs:: write ( dir. path (). join ( "app, data/private/no.js" ), "export {};" ). unwrap (); 214 fs:: write ( 215 dir. path (). join ( "main.js" ), 216r#" 217import {one} from './app, data/one.ts'; import {two} from './other/two.js'; 218try { await import('./app, data/private/no.js'); throw new Error('Unexpected import'); } 219catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } 220console.log(one + two); 221"# , 222) 223. unwrap (); 224for argsin [ 225vec! [ 226"-0" , 227"--allow-read=app, data" , 228"--allow-js-exec=other" , 229"--deny-js-exec=app, data/private" , 230"main.js" , 231], 232vec! [ 233"--deny-js-exec=app, data/private" , 234"main.js" , 235"--allow-js-exec=other" , 236"--allow-read=app, data" , 237"-0" , 238], 239] { 240let output =run ( dir. path (), & args); 241assert! ( 242 output. status . success (), 243"{}" , 244String :: from_utf8_lossy ( & output. stderr ) 245); 246assert_eq! ( output. stdout , b"3\n" ); 247} 248let output =run ( dir. path (), & [ "-0" , "main.js" ]); 249assert! ( !output. status . success ()); 250assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "PermissionDenied: js-exec" )); 251} 252 253# [ test ] 254fn runtime_pledges_recheck_cached_imports_and_cannot_restore_authority () { 255let dir = tempfile:: tempdir (). unwrap (); 256 fs:: write ( dir. path (). join ( "a.js" ), "export const value = 42;" ). unwrap (); 257 fs:: write ( dir. path (). join ( "b.js" ), "export const value = 7;" ). unwrap (); 258 fs:: write ( 259 dir. path (). join ( "main.js" ), 260r#" 261import { pledge } from 'avril:std'; 262const a = await import('./a.js'); 263pledge({jsExec:{deny:['./a.js']}}); 264for (const path of ['./a.js', new URL('./a.js', import.meta.url)]) { 265try { await import(path); throw new Error('Unexpected cached import'); } 266catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } 267} 268pledge({jsExec:{allow:['.']}}); 269console.log(a.value, (await import('./b.js')).value); 270pledge({jsExec:false}); 271pledge({jsExec:{allow:['.']}}); 272try { await import('./b.js'); throw new Error('Restored authority'); } 273catch (e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } 274console.log('restricted'); 275"# , 276) 277. unwrap (); 278let output =run ( dir. path (), & [ "main.js" ]); 279assert! ( 280 output. status . success (), 281"{}" , 282String :: from_utf8_lossy ( & output. stderr ) 283); 284assert_eq! ( output. stdout , b"42 7\nrestricted\n" ); 285} 286 287# [ test ] 288fn invalid_pledges_do_not_partially_commit_and_captured_intrinsics_keep_working () { 289let dir = tempfile:: tempdir (). unwrap (); 290 fs:: write ( dir. path (). join ( "dep.js" ), "export const value = 42;" ). unwrap (); 291 fs:: write ( 292 dir. path (). join ( "main.js" ), 293r#" 294import { pledge } from 'avril:std'; 295for (const request of [{jsExec:false,net:{deny:['a,b']}}, {jsExec:false,read:{deny:['\0']}}, 296{jsExec:false,unknown:false}, {jsExec:{allow:'not-an-array'}}, {jsExec:true}, null]) { 297try { pledge(request); throw new Error('Expected invalid pledge'); } 298catch(e) { if (!(e instanceof TypeError)) throw e; } 299} 300console.log((await import('./dep.js')).value); 301const strings = String.prototype; 302const poison = () => { throw new Error('Poisoned intrinsic'); }; 303globalThis.String = poison; strings.toWellFormed = poison; 304Array.prototype.push = poison; 305Object.defineProperty(Array.prototype,'1',{set:poison,configurable:true}); 306pledge({net:{allow:['a.example','b.example']},jsExec:{deny:['dep.js']}}); 307delete Array.prototype[1]; 308try { await import('./dep.js'); throw new Error('Unexpected import'); } 309catch(e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } 310console.log('atomic'); 311"# , 312) 313. unwrap (); 314let output =run ( dir. path (), & [ "main.js" ]); 315assert! ( 316 output. status . success (), 317"{}" , 318String :: from_utf8_lossy ( & output. stderr ) 319); 320assert_eq! ( output. stdout , b"42\natomic\n" ); 321} 322 323# [ cfg ( unix )] 324# [ test ] 325fn symlink_aliases_cannot_escape_grants_or_execution_denials () { 326use std:: os:: unix:: fs:: symlink; 327let dir = tempfile:: tempdir (). unwrap (); 328 fs:: create_dir ( dir. path (). join ( "app" )). unwrap (); 329 fs:: create_dir ( dir. path (). join ( "private" )). unwrap (); 330 fs:: write ( dir. path (). join ( "private/no.js" ), "export {};" ). unwrap (); 331symlink ( "../private/no.js" , dir. path (). join ( "app/alias.js" )). unwrap (); 332 fs:: write ( 333 dir. path (). join ( "main.js" ), 334"await import('./app/alias.js');" , 335) 336. unwrap (); 337for argsin [ 338vec! [ "-0" , "--allow-read=app" , "main.js" ], 339vec! [ "--deny-js-exec=private" , "main.js" ], 340] { 341let output =run ( dir. path (), & args); 342assert! ( !output. status . success ()); 343assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "PermissionDenied: js-exec" )); 344} 345let output =run ( 346 dir. path (), 347& [ "-0" , "--allow-js-exec=app/alias.js" , "main.js" ], 348); 349assert! ( 350 output. status . success (), 351"{}" , 352String :: from_utf8_lossy ( & output. stderr ) 353); 354} 355 356# [ test ] 357fn cli_rejects_malformed_grants_and_unknown_options () { 358let dir = tempfile:: tempdir (). unwrap (); 359 fs:: write ( dir. path (). join ( "main.js" ), "console.log('executed');" ). unwrap (); 360for flagin [ 361"--allow-net=a,b" , 362"--allow-net=host:65536" , 363"--allow-net=" , 364"--allow-net=https://example.com" , 365"--allow-read=" , 366"--allow-js-exec" , 367"--allow-unknown=x" , 368"--unknown=x" , 369] { 370let output =run ( dir. path (), & [ flag, "main.js" ]); 371assert! ( !output. status . success (), "{flag}" ); 372assert! ( output. stdout . is_empty ()); 373} 374let output =run ( 375 dir. path (), 376& [ 377"-0" , 378"--allow-net=host1" , 379"--allow-net=host2:443" , 380"main.js" , 381], 382); 383assert! ( output. status . success ()); 384assert_eq! ( output. stdout , b"executed\n" ); 385} 386 387# [ test ] 388fn queued_import_of_an_unevaluated_cached_module_is_rechecked () { 389let dir = tempfile:: tempdir (). unwrap (); 390 fs:: write ( 391 dir. path (). join ( "graph.js" ), 392"import './throw.js'; import './later.js';" , 393) 394. unwrap (); 395 fs:: write ( 396 dir. path (). join ( "throw.js" ), 397"throw new Error('Stop this graph');" , 398) 399. unwrap (); 400 fs:: write ( 401 dir. path (). join ( "later.js" ), 402"import './dep.js'; globalThis.executed = true;" , 403) 404. unwrap (); 405 fs:: write ( 406 dir. path (). join ( "dep.js" ), 407"globalThis.executedDependency = true;" , 408) 409. unwrap (); 410for ( file, source) in [ 411( 412"diamond.js" , 413"import './throw.js'; import './left.js'; import './right.js';" , 414), 415( "left.js" , "import './shared.js';" ), 416( "right.js" , "import './shared.js';" ), 417( 418"shared.js" , 419"import './dep.js'; globalThis.executed = true;" , 420), 421( "cycle.js" , "import './throw.js'; import './x.js';" ), 422( "x.js" , "import './y.js'; globalThis.executed = true;" ), 423( "y.js" , "import './x.js'; import './dep.js';" ), 424] { 425 fs:: write ( dir. path (). join ( file), source). unwrap (); 426} 427for ( graph, parent, denied) in [ 428( "./graph.js" , "./later.js" , "./later.js" ), 429( "./graph.js" , "./later.js" , "./dep.js" ), 430( "./diamond.js" , "./left.js" , "./dep.js" ), 431( "./cycle.js" , "./x.js" , "./dep.js" ), 432] { 433let source =r#" 434import { pledge } from 'avril:std'; 435await import('GRAPH').catch(() => {}); 436const pending = import('PARENT'); 437pledge({jsExec:{deny:['DENIED']}}); 438try { await pending; throw new Error('Expected queued denial'); } 439catch(e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } 440console.log(globalThis.executed === undefined && globalThis.executedDependency === undefined); 441"# . replace ( "DENIED" , denied). replace ( "GRAPH" , graph). replace ( "PARENT" , parent); 442 fs:: write ( dir. path (). join ( "main.js" ), source). unwrap (); 443let output =run ( dir. path (), & [ "main.js" ]); 444assert! ( 445 output. status . success (), 446"{}" , 447String :: from_utf8_lossy ( & output. stderr ) 448); 449assert_eq! ( output. stdout , b"true\n" ); 450} 451} 452 453# [ test ] 454fn denied_imports_do_not_disclose_canonical_targets_or_resolution_errors () { 455let dir = tempfile:: tempdir (). unwrap (); 456 fs:: write ( dir. path (). join ( "existing.js" ), "export {};" ). unwrap (); 457# [ cfg ( unix )] 458 std:: os:: unix:: fs:: symlink ( "existing.js" , dir. path (). join ( "alias.js" )). unwrap (); 459 fs:: write ( 460 dir. path (). join ( "main.js" ), 461r#" 462for(const specifier of ['./existing.js', './missing.js', './alias.js']) { 463try { await import(specifier); throw new Error('Expected denial'); } 464catch(e) { if (e.message !== 'PermissionDenied: js-exec ' + specifier) throw e; } 465} 466console.log('denied'); 467"# , 468) 469. unwrap (); 470let output =run ( dir. path (), & [ "-0" , "main.js" ]); 471assert! ( 472 output. status . success (), 473"{}" , 474String :: from_utf8_lossy ( & output. stderr ) 475); 476assert_eq! ( output. stdout , b"denied\n" ); 477} 478 479# [ test ] 480fn pledge_is_sealed_and_accepts_module_relative_file_urls () { 481let dir = tempfile:: tempdir (). unwrap (); 482 fs:: create_dir ( dir. path (). join ( "app" )). unwrap (); 483 fs:: write ( 484 dir. path (). join ( "app/blocked.js" ), 485"throw new Error('Should not execute');" , 486) 487. unwrap (); 488 fs:: write ( 489 dir. path (). join ( "app/main.js" ), 490r#" 491import { pledge } from 'avril:std'; 492import * as std from 'avril:std'; 493if (Symbol.for('__avril.base') in globalThis) throw new Error('Leaked base'); 494try { std.pledge = () => {}; throw new Error('Replaced pledge'); } 495catch(e) { if (!(e instanceof TypeError)) throw e; } 496pledge({jsExec:{deny:[new URL('./blocked.js',import.meta.url)]}}); 497try { await import('./blocked.js'); throw new Error('Expected denial'); } 498catch(e) { if (!e.message.includes('PermissionDenied: js-exec')) throw e; } 499console.log('sealed'); 500"# , 501) 502. unwrap (); 503let output =run ( dir. path (), & [ "app/main.js" ]); 504assert! ( 505 output. status . success (), 506"{}" , 507String :: from_utf8_lossy ( & output. stderr ) 508); 509assert_eq! ( output. stdout , b"sealed\n" ); 510} 511 512# [ test ] 513fn inherited_byte_streams_remain_available_with_zero_and_after_pledging () { 514use std:: io:: Write ; 515use std:: process:: Stdio ; 516let dir = tempfile:: tempdir (). unwrap (); 517 fs:: write ( 518 dir. path (). join ( "main.js" ), 519r#" 520import { pledge, stdin, stdout, stderr } from 'avril:std'; 521pledge({net:false,read:false,jsExec:false}); 522await stderr.getWriter().write(new Uint8Array([5,6])); 523await stdin.pipeTo(stdout); 524"# , 525) 526. unwrap (); 527let mut child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 528. arg ( "run" ) 529. current_dir ( dir. path ()) 530. args ([ "-0" , "main.js" ]) 531. stdin ( Stdio :: piped ()) 532. stdout ( Stdio :: piped ()) 533. stderr ( Stdio :: piped ()) 534. spawn () 535. unwrap (); 536let input =[ 0 , 1 , 127 , 128 , 255 ]; 537 child. stdin . take (). unwrap (). write_all ( & input). unwrap (); 538let output = child. wait_with_output (). unwrap (); 539assert! ( 540 output. status . success (), 541"{}" , 542String :: from_utf8_lossy ( & output. stderr ) 543); 544assert_eq! ( output. stdout , input); 545assert_eq! ( output. stderr , [ 5 , 6 ]); 546} 547 548# [ test ] 549fn write_grants_are_independent_and_missing_files_are_exact () { 550let dir = tempfile:: tempdir (). unwrap (); 551let target = |name : & str |PathTarget :: for_creation ( dir. path (). join ( name)). unwrap (); 552let caps =policy ( 553true , 554& [ 555( Category :: Write , false , "new.txt" ), 556( Category :: Write , false , "future/" ), 557( Category :: Write , true , "future/private/" ), 558( Category :: Write , false , "a, b" ), 559( Category :: Write , false , "second" ), 560], 561 dir. path (), 562); 563for pathin [ "new.txt" , "future" , "future/file" , "a, b" , "second" ] { 564assert! ( caps. check_write ( & target ( path)). is_ok (), "{path}" ); 565} 566for pathin [ 567"new.txt/child" , 568"future/private" , 569"future/private/file" , 570"a" , 571"b" , 572"other" , 573] { 574assert! ( caps. check_write ( & target ( path)). is_err (), "{path}" ); 575} 576assert! ( caps. check_read ( & target ( "new.txt" )). is_err ()); 577assert! ( caps. check_js_exec ( & target ( "new.txt" )). is_err ()); 578assert! ( 579policy ( false , & [], dir. path ()) 580. check_write ( & target ( "anything" )) 581. is_ok () 582); 583assert! ( 584policy ( true , & [], dir. path ()) 585. check_write ( & target ( "anything" )) 586. is_err () 587); 588} 589 590# [ test ] 591fn write_pledges_are_atomic_monotonic_and_snapshot_for_children () { 592let dir = tempfile:: tempdir (). unwrap (); 593let target =PathTarget :: for_creation ( dir. path (). join ( "new.txt" )). unwrap (); 594let caps =policy ( false , & [], dir. path ()); 595let child =Capabilities :: inherit ( caps. clone ()); 596assert! ( 597 caps. pledge ( vec! [ 598( Category :: Write , Some ( vec! []), vec! []), 599( Category :: Read , None , vec! [ "\0" . into ()]) 600]) 601. is_err () 602); 603assert! ( child. check_write ( & target). is_ok ()); 604 caps. pledge ( vec! [( 605Category :: Write , 606Some ( vec! [ "new.txt" . into ()]), 607vec! [], 608)]) 609. unwrap (); 610assert! ( child. check_write ( & target). is_ok ()); 611 child 612. pledge ( vec! [( Category :: Write , Some ( vec! []), vec! [])]) 613. unwrap (); 614 child 615. pledge ( vec! [( 616Category :: Write , 617Some ( vec! [ "new.txt" . into ()]), 618vec! [], 619)]) 620. unwrap (); 621assert! ( child. check_write ( & target). is_err ()); 622// Existing workers keep their snapshot; later ones inherit the denial. 623let other =Capabilities :: inherit ( caps. clone ()); 624assert! ( other. check_write ( & target). is_ok ()); 625 caps. pledge ( vec! [( Category :: Write , None , vec! [ "new.txt" . into ()])]) 626. unwrap (); 627assert! ( caps. check_write ( & target). is_err ()); 628assert! ( other. check_write ( & target). is_ok ()); 629let narrowed =Capabilities :: inherit ( caps. clone ()); 630assert! ( narrowed. check_write ( & target). is_err ()); 631} 632 633# [ test ] 634fn creation_targets_resolve_existing_prefixes_without_inventing_parent_traversal () { 635let dir = tempfile:: tempdir (). unwrap (); 636 fs:: create_dir ( dir. path (). join ( "real" )). unwrap (); 637let path = dir. path (). join ( "real/missing/deeper/file" ); 638assert_eq! ( PathTarget :: for_creation ( & path). unwrap (). as_path (), path); 639assert! ( PathTarget :: for_creation ( dir. path (). join ( "real/missing/../file" )). is_err ()); 640let mut initial =Initial :: new ( true ); 641assert! ( 642 initial 643. grant ( Category :: Write , false , "real/missing/../file" , dir. path ()) 644. is_err () 645); 646# [ cfg ( unix )] 647{ 648 std:: os:: unix:: fs:: symlink ( "real" , dir. path (). join ( "alias" )). unwrap (); 649assert_eq! ( 650PathTarget :: for_creation ( dir. path (). join ( "alias/missing/file" )) 651. unwrap () 652. as_path (), 653 dir. path (). join ( "real/missing/file" ) 654); 655let caps =policy ( true , & [( Category :: Write , false , "alias/new" )], dir. path ()); 656assert! ( 657 caps. check_write ( & PathTarget :: for_creation ( dir. path (). join ( "real/new" )). unwrap ()) 658. is_ok () 659); 660} 661} 662 663# [ test ] 664fn cli_and_public_pledge_accept_write_without_granting_module_execution () { 665let dir = tempfile:: tempdir (). unwrap (); 666 fs:: write ( 667 dir. path (). join ( "dep.js" ), 668"throw Error('Unexpected execution')" , 669) 670. unwrap (); 671 fs:: write ( dir. path (). join ( "main.js" ), r#" 672import {pledge} from 'avril:std'; 673pledge({write:{allow:['a, b','future/'],deny:['future/private/']}}); 674try{pledge({write:false,unknown:false});throw Error('Invalid pledge');}catch(e){if(!(e instanceof TypeError))throw e;} 675pledge({write:false}); 676try{await import('./dep.js');throw Error('Write grants authorize execution');}catch(e){if(!e.message.includes('PermissionDenied: js-exec'))throw e;} 677console.log('write capability'); 678"# ). unwrap (); 679let output =run ( 680 dir. path (), 681& [ 682"-0" , 683"--allow-write=a, b" , 684"--allow-write=future/" , 685"--deny-write=future/private/" , 686"main.js" , 687], 688); 689assert! ( 690 output. status . success (), 691"{}" , 692String :: from_utf8_lossy ( & output. stderr ) 693); 694assert_eq! ( output. stdout , b"write capability\n" ); 695} 696 697# [ test ] 698fn path_denials_cover_descendants_after_missing_or_file_targets_become_directories () { 699let dir = tempfile:: tempdir (). unwrap (); 700 fs:: write ( dir. path (). join ( "file" ), "" ). unwrap (); 701let caps =policy ( 702false , 703& [ 704( Category :: Write , true , "future" ), 705( Category :: Write , true , "file" ), 706( Category :: Read , true , "file" ), 707], 708 dir. path (), 709); 710 fs:: remove_file ( dir. path (). join ( "file" )). unwrap (); 711 fs:: create_dir ( dir. path (). join ( "file" )). unwrap (); 712for pathin [ "future/secret" , "file/secret" ] { 713let target =PathTarget :: for_creation ( dir. path (). join ( path)). unwrap (); 714assert! ( caps. check_write ( & target). is_err ()); 715} 716assert! ( 717 caps. check_read ( & PathTarget :: for_creation ( dir. path (). join ( "file/secret" )). unwrap ()) 718. is_err () 719); 720let other =policy ( false , & [], dir. path ()); 721 other 722. pledge ( vec! [( Category :: Write , None , vec! [ "missing" . into ()])]) 723. unwrap (); 724assert! ( 725 other 726. check_write ( & PathTarget :: for_creation ( dir. path (). join ( "missing/child" )). unwrap ()) 727. is_err () 728); 729} 730 731# [ test ] 732fn remote_execution_grants_are_origin_scoped_canonical_and_monotonic () { 733let dir = tempfile:: tempdir (). unwrap (); 734let mut initial =Initial :: new ( true ); 735 initial 736. grant ( 737Category :: JsExec , 738false , 739"https://jsr.io/@test/pkg/" , 740 dir. path (), 741) 742. unwrap (); 743 initial 744. grant ( 745Category :: JsExec , 746true , 747"https://jsr.io/@test/pkg/1.0.0/blocked.ts" , 748 dir. path (), 749) 750. unwrap (); 751let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 752let allowed = url:: Url :: parse ( "https://jsr.io/@test/pkg/1.0.0/mod.ts" ). unwrap (); 753 caps. check_remote_exec ( & allowed). unwrap (); 754 caps. check_remote_acquire ( & url:: Url :: parse ( "https://jsr.io/@test/pkg/" ). unwrap ()) 755. unwrap (); 756for namein [ 757"http://jsr.io/@test/pkg/1.0.0/mod.ts" , 758"https://jsr.io:444/@test/pkg/1.0.0/mod.ts" , 759"https://jsr.io/@test/pkg-other/1.0.0/mod.ts" , 760"https://jsr.io/@test/pkg/1.0.0/blocked.ts" , 761"https://jsr.io/@test/pkg/1.0.0/blocked%2Ets" , 762"https://jsr.io/@test/pkg/1.0.0/blocked.ts/child" , 763"https://jsr.io/@test/pkg/1.0.0/%2fblocked.ts" , 764] { 765assert! ( 766 caps. check_remote_exec ( & url:: Url :: parse ( name). unwrap ()) 767. is_err (), 768"{name}" 769); 770} 771let child =Capabilities :: inherit ( caps. clone ()); 772assert! ( 773 caps. pledge ( vec! [( 774Category :: JsExec , 775Some ( vec! [ "https://jsr.io/" . into (), "https://[invalid" . into ()]), 776vec! [] 777)]) 778. is_err () 779); 780 child. check_remote_exec ( & allowed). unwrap (); 781 caps. pledge ( vec! [( 782Category :: JsExec , 783Some ( vec! [ dir. path (). to_string_lossy (). into_owned ()]), 784vec! [], 785)]) 786. unwrap (); 787assert! ( caps. check_remote_exec ( & allowed). is_err ()); 788assert! ( child. check_remote_exec ( & allowed). is_ok ()); 789assert! ( 790 caps. check_remote_acquire ( & url:: Url :: parse ( "https://jsr.io/@test/pkg/" ). unwrap ()) 791. is_err () 792); 793assert! ( 794 child 795. check_remote_acquire ( & url:: Url :: parse ( "https://jsr.io/@test/pkg/" ). unwrap ()) 796. is_ok () 797); 798 caps. pledge ( vec! [( 799Category :: JsExec , 800Some ( vec! [ "https://jsr.io/" . into ()]), 801vec! [], 802)]) 803. unwrap (); 804assert! ( caps. check_remote_exec ( & allowed). is_err ()); 805assert! ( child. check_remote_exec ( & allowed). is_ok ()); 806let later =Capabilities :: inherit ( caps. clone ()); 807assert! ( later. check_remote_exec ( & allowed). is_err ()); 808} 809 810# [ test ] 811fn executable_grants_are_independent_monotonic_and_snapshot_for_workers () { 812let dir = tempfile:: tempdir (). unwrap (); 813 fs:: write ( dir. path (). join ( "tool" ), "" ). unwrap (); 814 fs:: write ( dir. path (). join ( "other" ), "" ). unwrap (); 815let tool =PathTarget :: resolve ( dir. path (). join ( "tool" )). unwrap (); 816let other =PathTarget :: resolve ( dir. path (). join ( "other" )). unwrap (); 817let read =policy ( true , & [( Category :: Read , false , "." )], dir. path ()); 818let js =policy ( true , & [( Category :: JsExec , false , "." )], dir. path ()); 819assert! ( read. check_run ( & tool). is_err ()); 820assert! ( js. check_run ( & tool). is_err ()); 821let caps =policy ( true , & [( Category :: Run , false , "tool" )], dir. path ()); 822assert! ( caps. check_run ( & tool). is_ok ()); 823assert! ( caps. check_run ( & other). is_err ()); 824assert! ( caps. check_read ( & tool). is_err ()); 825assert! ( caps. check_js_exec ( & tool). is_err ()); 826let child =Capabilities :: inherit ( caps. clone ()); 827assert! ( child. check_run ( & tool). is_ok ()); 828assert! ( 829 caps. pledge ( vec! [ 830( Category :: Run , Some ( vec! []), vec! []), 831( Category :: Net , None , vec! [ "invalid,host" . into ()]) 832]) 833. is_err () 834); 835assert! ( caps. check_run ( & tool). is_ok ()); 836 caps. pledge ( vec! [( Category :: Run , None , vec! [ "tool" . into ()])]) 837. unwrap (); 838assert! ( caps. check_run ( & tool). is_err ()); 839assert! ( child. check_run ( & tool). is_ok ()); 840 caps. pledge ( vec! [( Category :: Run , Some ( vec! [ "tool" . into ()]), vec! [])]) 841. unwrap (); 842assert! ( caps. check_run ( & tool). is_err ()); 843assert! ( child. check_run ( & tool). is_ok ()); 844let later =Capabilities :: inherit ( caps. clone ()); 845assert! ( later. check_run ( & tool). is_err ()); 846} 847 848# [ test ] 849fn missing_executable_grants_are_exact_and_file_urls_work () { 850let dir = tempfile:: tempdir (). unwrap (); 851let name = dir. path (). join ( "tool" ); 852let file_url = url:: Url :: from_file_path ( & name). unwrap (); 853let caps =policy ( 854true , 855& [( Category :: Run , false , file_url. as_str ())], 856 dir. path (), 857); 858 fs:: create_dir ( & name). unwrap (); 859 fs:: write ( name. join ( "child" ), "" ). unwrap (); 860assert! ( caps. check_run ( & PathTarget :: resolve ( & name). unwrap ()). is_ok ()); 861assert! ( 862 caps. check_run ( & PathTarget :: resolve ( name. join ( "child" )). unwrap ()) 863. is_err () 864); 865} 866 867# [ test ] 868fn worker_snapshots_preserve_restrictions_and_isolate_descendants () { 869let dir = tempfile:: tempdir (). unwrap (); 870 fs:: create_dir ( dir. path (). join ( "app" )). unwrap (); 871for filein [ "app/one.js" , "app/two.js" , "tool" , "other" ] { 872 fs:: write ( dir. path (). join ( file), "" ). unwrap (); 873} 874let one =PathTarget :: resolve ( dir. path (). join ( "app/one.js" )). unwrap (); 875let two =PathTarget :: resolve ( dir. path (). join ( "app/two.js" )). unwrap (); 876let tool =PathTarget :: resolve ( dir. path (). join ( "tool" )). unwrap (); 877let other =PathTarget :: resolve ( dir. path (). join ( "other" )). unwrap (); 878let write_target = |name : & str |PathTarget :: for_creation ( dir. path (). join ( name)). unwrap (); 879let caps =policy ( 880true , 881& [ 882( Category :: Net , false , "a.example" ), 883( Category :: Net , false , "b.example" ), 884( Category :: Read , false , "app" ), 885( Category :: Write , false , "app" ), 886( Category :: JsExec , false , "app" ), 887( Category :: Env , false , "KEEP" ), 888( Category :: Env , false , "DROP" ), 889( Category :: Sys , false , "hostname" ), 890( Category :: Run , false , "tool" ), 891( Category :: Ffi , false , "tool" ), 892], 893 dir. path (), 894); 895 caps. pledge ( vec! [ 896( 897Category :: Net , 898Some ( vec! [ "a.example" . into (), "b.example" . into ()]), 899vec! [], 900), 901( Category :: Read , Some ( vec! [ "app" . into ()]), vec! []), 902( Category :: Write , Some ( vec! [ "app" . into ()]), vec! []), 903]) 904. unwrap (); 905 caps. pledge ( vec! [ 906( 907Category :: Net , 908Some ( vec! [ "a.example" . into ()]), 909vec! [ "a.example:443" . into ()], 910), 911( Category :: Env , None , vec! [ "DROP" . into ()]), 912( Category :: Read , None , vec! [ "app/two.js" . into ()]), 913( Category :: Write , None , vec! [ "app/two.js" . into ()]), 914( Category :: JsExec , None , vec! [ "app/two.js" . into ()]), 915]) 916. unwrap (); 917let worker =Capabilities :: inherit ( caps. clone ()); 918let sibling =Capabilities :: inherit ( caps. clone ()); 919assert! ( worker. check_net ( "a.example" , 80 ). is_ok ()); 920assert! ( worker. check_net ( "a.example" , 443 ). is_err ()); 921assert! ( worker. check_net ( "b.example" , 80 ). is_err ()); 922assert! ( worker. check_read ( & one). is_ok ()); 923assert! ( worker. check_read ( & two). is_err ()); 924assert! ( worker. check_write ( & write_target ( "app/one.js" )). is_ok ()); 925assert! ( worker. check_write ( & write_target ( "app/two.js" )). is_err ()); 926assert! ( worker. check_js_exec ( & one). is_ok ()); 927assert! ( worker. check_js_exec ( & two). is_err ()); 928assert! ( worker. check_env ( "KEEP" ). is_ok ()); 929assert! ( worker. check_env ( "DROP" ). is_err ()); 930assert! ( worker. check_sys ( "hostname" ). is_ok ()); 931assert! ( worker. check_sys ( "uptime" ). is_err ()); 932assert! ( worker. check_run ( & tool). is_ok ()); 933assert! ( worker. check_run ( & other). is_err ()); 934assert! ( worker. check_ffi ( & tool). is_ok ()); 935assert! ( worker. check_ffi ( & other). is_err ()); 936// Intersected allows cannot be widened back. 937 worker 938. pledge ( vec! [( 939Category :: Net , 940Some ( vec! [ "a.example" . into (), "b.example" . into ()]), 941vec! [], 942)]) 943. unwrap (); 944assert! ( worker. check_net ( "a.example" , 80 ). is_ok ()); 945assert! ( worker. check_net ( "b.example" , 80 ). is_err ()); 946 worker 947. pledge ( vec! [( Category :: Env , None , vec! [ "KEEP" . into ()])]) 948. unwrap (); 949assert! ( worker. check_env ( "KEEP" ). is_err ()); 950assert! ( sibling. check_env ( "KEEP" ). is_ok ()); 951assert! ( caps. check_env ( "KEEP" ). is_ok ()); 952let nested =Capabilities :: inherit ( worker. clone ()); 953assert! ( nested. check_net ( "a.example" , 80 ). is_ok ()); 954assert! ( nested. check_net ( "b.example" , 80 ). is_err ()); 955assert! ( nested. check_env ( "KEEP" ). is_err ()); 956assert! ( nested. check_env ( "DROP" ). is_err ()); 957assert! ( nested. check_read ( & one). is_ok ()); 958 caps. pledge ( vec! [( Category :: Net , None , vec! [ "a.example:80" . into ()])]) 959. unwrap (); 960assert! ( caps. check_net ( "a.example" , 80 ). is_err ()); 961assert! ( worker. check_net ( "a.example" , 80 ). is_ok ()); 962assert! ( sibling. check_net ( "a.example" , 80 ). is_ok ()); 963assert! ( nested. check_net ( "a.example" , 80 ). is_ok ()); 964let later =Capabilities :: inherit ( caps. clone ()); 965assert! ( later. check_net ( "a.example" , 80 ). is_err ()); 966assert! ( later. check_net ( "a.example" , 8443 ). is_ok ()); 967assert! ( later. check_read ( & one). is_ok ()); 968assert! ( later. check_env ( "KEEP" ). is_ok ()); 969} 970 971# [ test ] 972fn nested_workers_snapshot_their_direct_parent () { 973let dir = tempfile:: tempdir (). unwrap (); 974let caps =policy ( false , & [], dir. path ()); 975let worker =Capabilities :: inherit ( caps. clone ()); 976 caps. pledge ( vec! [( Category :: Net , None , vec! [ "a.example:80" . into ()])]) 977. unwrap (); 978assert! ( caps. check_net ( "a.example" , 80 ). is_err ()); 979// Created after the root pledge, yet keeps its direct parent's snapshot. 980let nested =Capabilities :: inherit ( worker. clone ()); 981assert! ( worker. check_net ( "a.example" , 80 ). is_ok ()); 982assert! ( nested. check_net ( "a.example" , 80 ). is_ok ()); 983// A later pledge by the direct parent leaves the existing nested worker 984// alone but narrows subsequent ones. 985 worker 986. pledge ( vec! [( Category :: Net , None , vec! [ "a.example:443" . into ()])]) 987. unwrap (); 988assert! ( worker. check_net ( "a.example" , 443 ). is_err ()); 989assert! ( nested. check_net ( "a.example" , 443 ). is_ok ()); 990let later =Capabilities :: inherit ( worker. clone ()); 991assert! ( later. check_net ( "a.example" , 443 ). is_err ()); 992assert! ( later. check_net ( "a.example" , 80 ). is_ok ()); 993}