char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f4a0f5b
main
1pub mod fs; 2 3use std:: borrow:: Cow ; 4use std:: path::{ Path , PathBuf }; 5use std:: sync::{ Arc , RwLock }; 6 7# [ derive ( Clone , Debug , PartialEq , Eq , thiserror :: Error )] 8pub enum CapabilitiesError { 9# [ error ( "Unknown capability: {0}" )] 10UnknownCapability ( String ), 11# [ error ( "Unknown system information name: {0}" )] 12UnknownSys ( String ), 13# [ error ( "Environment variable names must be nonempty and contain neither NUL nor '='" )] 14InvalidEnvName , 15# [ error ( "Invalid network grant: {0}" )] 16InvalidNetGrant ( String ), 17# [ error ( "Invalid execution URL: {0}" )] 18InvalidExecUrl ( String ), 19# [ error ( "Invalid path grant: {0}" )] 20InvalidPathGrant ( String ), 21# [ error ( "Invalid file URL grant: {0}" )] 22InvalidFileUrlGrant ( String ), 23# [ error ( "File URL grants cannot contain queries or fragments" )] 24FileUrlQueryFragment , 25# [ error ( "Invalid empty or NUL-containing path grant" )] 26EmptyPathGrant , 27# [ error ( "Encoded path separators are not supported" )] 28EncodedSeparator , 29# [ error ( "Remote paths must be UTF-8" )] 30RemoteNotUtf8 , 31# [ error ( "Expected a hierarchical module URL" )] 32NotHierarchical , 33# [ error ( "Invalid remote path segment" )] 34InvalidRemoteSegment , 35# [ error ( "Network checks require a host and separate port" )] 36NetworkRequiresHostPort , 37# [ error ( "PermissionDenied: env {0}" )] 38EnvDenied ( String ), 39# [ error ( "PermissionDenied: sys {0}" )] 40SysDenied ( String ), 41# [ error ( "PermissionDenied: net {0}:{1}" )] 42NetDenied ( String , u16 ), 43# [ error ( "PermissionDenied: net {0}" )] 44NetUrlDenied ( String ), 45# [ error ( "PermissionDenied: read {}" , . 0 . display ())] 46ReadDenied ( PathBuf ), 47# [ error ( "PermissionDenied: write {}" , . 0 . display ())] 48WriteDenied ( PathBuf ), 49# [ error ( "PermissionDenied: run {}" , . 0 . display ())] 50RunDenied ( PathBuf ), 51# [ error ( "PermissionDenied: ffi {}" , . 0 . display ())] 52FfiDenied ( PathBuf ), 53# [ error ( "PermissionDenied: ffi" )] 54FfiDeniedAny , 55# [ error ( "PermissionDenied: read tree {}" , . 0 . display ())] 56ReadTreeDenied ( PathBuf ), 57# [ error ( "PermissionDenied: write tree {}" , . 0 . display ())] 58WriteTreeDenied ( PathBuf ), 59# [ error ( "PermissionDenied: js-exec {}" , . 0 . display ())] 60JsExecDenied ( PathBuf ), 61# [ error ( "PermissionDenied: js-exec {0}" )] 62RemoteAcquireDenied ( String ), 63# [ error ( "PermissionDenied: js-exec {0}" )] 64RemoteExecDenied ( String ), 65} 66 67# [ derive ( Clone , Copy , Debug , PartialEq , Eq )] 68pub enum Category { 69Net , 70Read , 71Write , 72JsExec , 73Env , 74Sys , 75Run , 76Ffi , 77} 78 79// The CLI spells one name js-exec and the JS pledge API spells it jsExec. 80impl std:: str:: FromStr for Category { 81type Err =CapabilitiesError ; 82 83fn from_str ( value : & str ) ->Result < Self , Self :: Err > { 84Ok ( match value{ 85"net" =>Self :: Net , 86"read" =>Self :: Read , 87"write" =>Self :: Write , 88"js-exec" |"jsExec" =>Self :: JsExec , 89"env" =>Self :: Env , 90"sys" =>Self :: Sys , 91"run" =>Self :: Run , 92"ffi" =>Self :: Ffi , 93 _ =>return Err ( CapabilitiesError :: UnknownCapability ( value. to_owned ())), 94}) 95} 96} 97 98pub fn sys_name ( value : & str ) ->Result < & str , CapabilitiesError > { 99match value{ 100"hostname" |"osRelease" |"uptime" |"loadavg" =>Ok ( value), 101 _ =>Err ( CapabilitiesError :: UnknownSys ( value. to_owned ())), 102} 103} 104 105pub fn env_name ( value : & str ) ->Result < Cow < ' _ , str >, CapabilitiesError > { 106if value. is_empty () || value. contains ([ '\0' , '=' ]) { 107return Err ( CapabilitiesError :: InvalidEnvName ); 108} 109if cfg! ( windows) { 110Ok ( Cow :: Owned ( value. to_ascii_uppercase ())) 111} else { 112Ok ( Cow :: Borrowed ( value)) 113} 114} 115 116# [ derive ( Clone , Debug , PartialEq , Eq )] 117pub struct NetGrant { 118host : url:: Host < String >, 119port : Option < u16 >, 120} 121 122impl NetGrant { 123pub fn parse ( value : & str ) ->Result < Self , CapabilitiesError > { 124let invalid = ||CapabilitiesError :: InvalidNetGrant ( value. to_owned ()); 125if value. is_empty () 126 || value 127. chars () 128. any ( |c| c. is_whitespace () ||",/@?#\\%*" . contains ( c)) 129{ 130return Err ( invalid ()); 131} 132let ( host, port) = value 133. rsplit_once ( ':' ) 134. filter ( |_| !value. ends_with ( ']' )) 135. map_or (( value, None ), |( host, port) |( host, Some ( port))); 136let port = port 137. map ( |port|{ 138if port. is_empty () || !port. bytes (). all ( |c| c. is_ascii_digit ()) { 139return Err ( invalid ()); 140} 141 port. parse ::< u16 >(). map_err ( |_|invalid ()) 142}) 143. transpose () ?; 144let mut host = url:: Host :: parse ( host). map_err ( |_|invalid ()) ?; 145if let url:: Host :: Domain ( name) =& mut host{ 146if name. ends_with ( '.' ) { 147 name. pop (); 148} 149if name. is_empty () { 150return Err ( invalid ()); 151} 152} 153Ok ( Self { host, port}) 154} 155 156fn matches ( & self , target : & Self ) ->bool { 157self . host == target. host &&( self . port . is_none () ||self . port == target. port ) 158} 159} 160 161pub fn canonical_remote_url ( url : & url:: Url ) ->Result < url:: Url , CapabilitiesError > { 162let lower = url. path (). to_ascii_lowercase (); 163if lower. contains ( "%2f" ) || lower. contains ( "%5c" ) { 164return Err ( CapabilitiesError :: EncodedSeparator ); 165} 166let decoded = percent_encoding:: percent_decode_str ( url. path ()) 167. decode_utf8 () 168. map_err ( |_|CapabilitiesError :: RemoteNotUtf8 ) ?; 169let mut canonical = url. clone (); 170{ 171let mut segments = canonical 172. path_segments_mut () 173. map_err ( |_|CapabilitiesError :: NotHierarchical ) ?; 174 segments. clear (); 175for segmentin decoded. strip_prefix ( '/' ). unwrap_or ( & decoded). split ( '/' ) { 176if matches! ( segment, "." |".." ) 177 || segment. contains ( '\\' ) 178 || segment. chars (). any ( char:: is_control) 179{ 180return Err ( CapabilitiesError :: InvalidRemoteSegment ); 181} 182 segments. push ( segment); 183} 184} 185Ok ( canonical) 186} 187 188# [ derive ( Clone , Debug )] 189pub struct RemoteGrant { 190url : url:: Url , 191subtree : bool , 192} 193 194impl RemoteGrant { 195fn parse ( value : & str ) ->Result < Self , CapabilitiesError > { 196let url = url:: Url :: parse ( value) 197. map_err ( |_|CapabilitiesError :: InvalidExecUrl ( value. to_owned ())) ?; 198if !matches! ( url. scheme (), "http" |"https" ) 199 || url. host (). is_none () 200 || !url. username (). is_empty () 201 || url. password (). is_some () 202 || url. query (). is_some () 203 || url. fragment (). is_some () 204{ 205return Err ( CapabilitiesError :: InvalidExecUrl ( value. to_owned ())); 206} 207let url =canonical_remote_url ( & url) ?; 208let subtree = url. path (). ends_with ( '/' ); 209Ok ( Self { url, subtree}) 210} 211 212fn overlaps ( & self , base : & url:: Url ) ->bool { 213self . matches ( base) 214 ||( self . url . origin () == base. origin () &&self . url . path (). starts_with ( base. path ())) 215} 216 217fn matches ( & self , target : & url:: Url ) ->bool { 218self . url . origin () == target. origin () 219 &&( self . url . path () == target. path () 220 ||self . subtree 221 &&if self . url . path (). ends_with ( '/' ) { 222 target. path (). starts_with ( self . url . path ()) 223} else { 224 target. path (). starts_with ( & format! ( "{}/" , self . url . path ())) 225}) 226} 227} 228 229fn remote_execution ( value : & str ) ->bool { 230 value. split_once ( ':' ). is_some_and ( |( scheme, _) |{ 231 scheme. eq_ignore_ascii_case ( "http" ) || scheme. eq_ignore_ascii_case ( "https" ) 232}) 233} 234 235# [ derive ( Clone , Debug )] 236pub struct PathTarget ( PathBuf ); 237 238impl PathTarget { 239pub fn resolve ( path : impl AsRef < Path >) -> std:: io:: Result < Self > { 240 path. as_ref (). canonicalize (). map ( Self ) 241} 242 243/// Resolve the existing prefix of a possibly missing path. Opening must refuse 244/// symlinks in every component, including dangling symlinks in the missing tail. 245pub fn for_creation ( path : impl AsRef < Path >) -> std:: io:: Result < Self > { 246let path =if path. as_ref (). is_absolute () { 247 path. as_ref (). to_owned () 248} else { 249 std:: env:: current_dir () ?. join ( path) 250}; 251resolve_prefix ( & path). map ( |( path, _) |Self ( path)) 252} 253 254/// Resolve parents but preserve the final directory entry for no-follow metadata. 255pub fn for_entry ( path : impl AsRef < Path >) -> std:: io:: Result < Self > { 256let path = path. as_ref (); 257match path. components (). next_back () { 258Some ( std:: path:: Component :: Normal ( name)) =>{ 259let parent =Self :: for_creation ( path. parent (). unwrap_or_else ( ||Path :: new ( "." ))) ?; 260Ok ( Self ( parent. into_path (). join ( name))) 261} 262 _ =>Self :: for_creation ( path), 263} 264} 265 266/// Like `for_entry`, but an existing entry takes its stored spelling, as 267/// canonicalization gives its parents, so case-insensitive filesystems 268/// cannot respell an executable past a run grant or denial. 269pub fn for_executable ( path : impl AsRef < Path >) -> std:: io:: Result < Self > { 270let Self ( path) =Self :: for_entry ( path) ?; 271stored_spelling ( path). map ( Self ) 272} 273 274pub fn child ( & self , name : impl AsRef < std:: ffi:: OsStr >) -> std:: io:: Result < Self > { 275let path =Path :: new ( name. as_ref ()); 276let mut components = path. components (); 277if !matches! ( components. next (), Some ( std:: path:: Component :: Normal ( _))) 278 || components. next (). is_some () 279{ 280return Err ( std:: io:: Error :: new ( 281 std:: io:: ErrorKind :: InvalidInput , 282"Expected a single entry name" , 283)); 284} 285Ok ( Self ( self . 0 . join ( path))) 286} 287 288pub fn as_path ( & self ) ->& Path { 289& self . 0 290} 291pub fn into_path ( self ) ->PathBuf { 292self . 0 293} 294} 295 296/// Lookup failures keep the lexical name: anything that stops this no-follow 297/// lookup also stops opening or executing the entry by that path. 298# [ cfg ( target_os = "macos" )] 299fn stored_spelling ( path : PathBuf ) -> std:: io:: Result < PathBuf > { 300use std:: os:: unix:: ffi::{ OsStrExt , OsStringExt }; 301use std:: os:: unix:: fs:: MetadataExt ; 302# [ repr ( C )] 303struct Attributes { 304length : u32 , 305name : libc:: attrreference_t , 306data : [ u8 ; 1024 ], 307} 308let text = std:: ffi:: CString :: new ( path. as_os_str (). as_bytes ()) ?; 309let mut request = libc:: attrlist { 310bitmapcount : libc:: ATTR_BIT_MAP_COUNT , 311reserved : 0 , 312commonattr : libc:: ATTR_CMN_NAME , 313volattr : 0 , 314dirattr : 0 , 315fileattr : 0 , 316forkattr : 0 , 317}; 318let mut attributes: Attributes =unsafe { std:: mem:: zeroed () }; 319if unsafe { 320 libc:: getattrlist ( 321 text. as_ptr (), 322( & raw mut request). cast (), 323( & raw mut attributes). cast (), 324 std:: mem:: size_of ::< Attributes >(), 325 libc:: FSOPT_NOFOLLOW , 326) 327} <0 328{ 329return Ok ( path); 330} 331// The NUL-terminated name lies at an offset from its reference. 332let start =( std:: mem:: offset_of!( Attributes , name) as isize 333 + attributes. name . attr_dataoffset as isize 334 - std:: mem:: offset_of!( Attributes , data) as isize ) as usize ; 335let length = attributes. name . attr_length as usize ; 336let Some ( name) = attributes 337. data 338. get ( start..start. saturating_add ( length)) 339. and_then ( |name| name. strip_suffix ( & [ 0 ])) 340else { 341return Err ( std:: io:: Error :: other ( "Malformed file name attribute" )); 342}; 343let stored = path. with_file_name ( std:: ffi:: OsString :: from_vec ( name. to_vec ())); 344// A hard-linked file may report another link's name; keep it only if 345// it names this same entry's file here. 346let same = |path : & Path | path. symlink_metadata (). map ( |meta|( meta. dev (), meta. ino ())); 347if stored != path &&same ( & stored). ok () !=same ( & path). ok () { 348return Ok ( path); 349} 350Ok ( stored) 351} 352 353# [ cfg ( windows )] 354fn stored_spelling ( path : PathBuf ) -> std:: io:: Result < PathBuf > { 355use std:: os:: windows:: ffi::{ OsStrExt , OsStringExt }; 356use std:: os:: windows:: fs:: OpenOptionsExt ; 357use std:: os:: windows:: io:: AsRawHandle ; 358use windows_sys:: Win32 :: Storage :: FileSystem ::{ 359FILE_FLAG_BACKUP_SEMANTICS , FILE_FLAG_OPEN_REPARSE_POINT , FILE_NAME_NORMALIZED , 360FILE_READ_ATTRIBUTES , FILE_SHARE_DELETE , FILE_SHARE_READ , FILE_SHARE_WRITE , 361GetFinalPathNameByHandleW , VOLUME_NAME_NONE , 362}; 363// A stream suffix names a different object than the entry. 364let name = path. file_name (). unwrap_or_default (); 365if name. encode_wide (). any ( |unit| unit == u16:: from ( b':' )) { 366return Err ( std:: io:: Error :: new ( 367 std:: io:: ErrorKind :: InvalidInput , 368"Invalid file name" , 369)); 370} 371// Opening a known name needs no permission to list its directory, so a 372// lookup that could fail where the launch succeeds must not stand in. 373let file =match std:: fs:: OpenOptions :: new () 374. access_mode ( FILE_READ_ATTRIBUTES ) 375. share_mode ( FILE_SHARE_READ |FILE_SHARE_WRITE |FILE_SHARE_DELETE ) 376. custom_flags ( FILE_FLAG_OPEN_REPARSE_POINT |FILE_FLAG_BACKUP_SEMANTICS ) 377. open ( & path) 378{ 379Ok ( file) => file, 380Err ( error) 381if matches! ( 382 error. kind (), 383 std:: io:: ErrorKind :: NotFound | std:: io:: ErrorKind :: NotADirectory 384) => 385{ 386return Ok ( path); 387} 388Err ( error) =>return Err ( error), 389}; 390let mut buffer =vec! [ 0u16 ; 512 ]; 391loop { 392let length =unsafe { 393GetFinalPathNameByHandleW ( 394 file. as_raw_handle (), 395 buffer. as_mut_ptr (), 396 buffer. len () as u32 , 397FILE_NAME_NORMALIZED |VOLUME_NAME_NONE , 398) 399} as usize ; 400if length ==0 { 401return Err ( std:: io:: Error :: last_os_error ()); 402} 403if length < buffer. len () { 404 buffer. truncate ( length); 405break ; 406} 407 buffer. resize ( length, 0 ); 408} 409// The long name as stored, also undoing 8.3 short names. 410let stored =PathBuf :: from ( std:: ffi:: OsString :: from_wide ( & buffer)); 411let name = stored 412. file_name () 413. ok_or_else ( || std:: io:: Error :: other ( "Entry has no stored name" )) ?; 414Ok ( path. with_file_name ( name)) 415} 416 417# [ cfg ( not ( any ( target_os = "macos" , windows )))] 418fn stored_spelling ( path : PathBuf ) -> std:: io:: Result < PathBuf > { 419Ok ( path) 420} 421 422fn resolve_prefix ( path : & Path ) -> std:: io:: Result <( PathBuf , bool )> { 423let mut input = path. to_owned (); 424let mut redirects =0 ; 425' resolve: loop { 426let mut ancestor = input. as_path (); 427let mut tail =Vec :: new (); 428let mut resolved =loop { 429match ancestor. canonicalize () { 430Ok ( path) =>break path, 431Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{ 432// A dangling link is an existing entry, not a missing lexical tail. 433match ancestor. symlink_metadata () { 434Ok ( metadata) if metadata. is_symlink () =>{ 435if redirects ==40 { 436return Err ( std:: io:: Error :: other ( 437"Too many symbolic links while resolving path" , 438)); 439} 440let contents = ancestor. read_link () ?; 441let mut target = ancestor. parent (). ok_or ( error) ?. join ( contents); 442for namein tail. into_iter (). rev () { 443 target. push ( name); 444} 445 input = target; 446 redirects +=1 ; 447continue ' resolve; 448} 449Err ( error) if error. kind () != std:: io:: ErrorKind :: NotFound =>{ 450return Err ( error); 451} 452 _ =>(), 453} 454let Some ( std:: path:: Component :: Normal ( name)) = 455 ancestor. components (). next_back () 456else { 457return Err ( std:: io:: Error :: new ( 458 std:: io:: ErrorKind :: NotFound , 459"Missing parent cannot resolve path components" , 460)); 461}; 462 tail. push ( name); 463 ancestor = ancestor. parent (). ok_or ( error) ?; 464} 465Err ( error) =>return Err ( error), 466} 467}; 468let existing = tail. is_empty (); 469for namein tail. into_iter (). rev () { 470 resolved. push ( name); 471} 472return Ok (( resolved, existing)); 473} 474} 475 476# [ derive ( Clone , Debug )] 477pub struct PathGrant { 478path : PathBuf , 479directory : bool , 480} 481 482impl PathGrant { 483pub fn parse ( value : & str , cwd : & Path ) ->Result < Self , CapabilitiesError > { 484Self :: parse_with_directory ( value, cwd, true , false ) 485} 486 487fn parse_file ( value : & str , cwd : & Path ) ->Result < Self , CapabilitiesError > { 488let directory = value. ends_with ( '/' ) ||( cfg! ( windows) && value. ends_with ( '\\' )); 489Self :: parse_with_directory ( value, cwd, directory, false ) 490} 491 492fn parse_run ( value : & str , cwd : & Path ) ->Result < Self , CapabilitiesError > { 493let directory = value. ends_with ( '/' ) ||( cfg! ( windows) && value. ends_with ( '\\' )); 494Self :: parse_with_directory ( value, cwd, directory, true ) 495} 496 497fn parse_with_directory ( 498value : & str , 499cwd : & Path , 500missing_directory : bool , 501executable_entry : bool , 502) ->Result < Self , CapabilitiesError > { 503if value. is_empty () || value. contains ( '\0' ) { 504return Err ( CapabilitiesError :: EmptyPathGrant ); 505} 506let path =if value 507. get ( ..5 ) 508. is_some_and ( |scheme| scheme. eq_ignore_ascii_case ( "file:" )) 509{ 510let url = url:: Url :: parse ( value) 511. map_err ( |_|CapabilitiesError :: InvalidFileUrlGrant ( value. to_owned ())) ?; 512if url. query (). is_some () || url. fragment (). is_some () { 513return Err ( CapabilitiesError :: FileUrlQueryFragment ); 514} 515 url. to_file_path () 516. map_err ( |_|CapabilitiesError :: InvalidFileUrlGrant ( value. to_owned ())) ? 517} else { 518 cwd. join ( value) 519}; 520// Multi-call executables must not turn one granted entry into authority for all aliases. 521if executable_entry && !missing_directory && !path. is_dir () { 522return Ok ( Self { 523path : PathTarget :: for_executable ( & path) 524. map_err ( |_|CapabilitiesError :: InvalidPathGrant ( value. to_owned ())) ? 525. into_path (), 526directory : false , 527}); 528} 529// Resolve existing symlinks, including the existing prefix of a not-yet-created path. 530let ( path, existing) =resolve_prefix ( & path) 531. map_err ( |_|CapabilitiesError :: InvalidPathGrant ( value. to_owned ())) ?; 532let directory =if existing{ 533 path. is_dir () 534} else { 535 missing_directory 536}; 537Ok ( Self { path, directory}) 538} 539 540fn matches ( & self , target : & Path ) ->bool { 541if self . directory { 542 target. starts_with ( & self . path ) 543} else { 544 target ==self . path 545} 546} 547} 548 549# [ derive ( Clone , Debug )] 550struct Policy < G > { 551all : bool , 552allow : Vec < G >, 553deny : Vec < G >, 554retain : Vec < Vec < G >>, 555} 556 557impl < G > Default for Policy < G > { 558fn default () ->Self { 559Self { 560all : true , 561allow : Vec :: new (), 562deny : Vec :: new (), 563retain : Vec :: new (), 564} 565} 566} 567 568impl < G > Policy < G > { 569fn granted ( & self , matches : impl Fn ( & G ) ->bool ) ->bool { 570self . all ||self . allow . iter (). any ( matches) 571} 572fn bounded ( & self , matches : impl Fn ( & G ) ->bool ) ->bool { 573 !self . deny . iter (). any ( & matches) &&self . retain . iter (). all ( |list| list. iter (). any ( & matches)) 574} 575fn permits ( & self , matches : impl Fn ( & G ) ->bool ) ->bool { 576self . granted ( & matches) &&self . bounded ( matches) 577} 578fn restrict ( & mut self , allow : Option < Vec < G >>, deny : Vec < G >) { 579if let Some ( allow) = allow{ 580self . retain . push ( allow); 581} 582self . deny . extend ( deny); 583} 584} 585 586# [ derive ( Clone , Default )] 587pub struct Initial { 588env : Policy < String >, 589sys : Policy < String >, 590net : Policy < NetGrant >, 591read : Policy < PathGrant >, 592write : Policy < PathGrant >, 593js_exec : Policy < PathGrant >, 594run : Policy < PathGrant >, 595ffi : Policy < PathGrant >, 596js_remote : Policy < RemoteGrant >, 597} 598 599impl Initial { 600pub fn new ( zero : bool ) ->Self { 601let mut initial =Self :: default (); 602 initial. env . all = !zero; 603 initial. sys . all = !zero; 604 initial. net . all = !zero; 605 initial. read . all = !zero; 606 initial. write . all = !zero; 607 initial. js_exec . all = !zero; 608 initial. run . all = !zero; 609 initial. ffi . all = !zero; 610 initial. js_remote . all = !zero; 611 initial 612} 613 614pub fn grant ( 615& mut self , 616category : Category , 617deny : bool , 618value : & str , 619cwd : & Path , 620) ->Result <(), CapabilitiesError > { 621if category ==Category :: JsExec &&remote_execution ( value) { 622let mut grant =RemoteGrant :: parse ( value) ?; 623if deny{ 624 grant. subtree =true ; 625self . js_remote . deny . push ( grant); 626} else { 627self . js_remote . allow . push ( grant); 628} 629return Ok (()); 630} 631match category{ 632Category :: Env =>{ 633let name =env_name ( value) ?. into_owned (); 634if deny{ 635self . env . deny . push ( name); 636} else { 637self . env . allow . push ( name); 638} 639} 640Category :: Sys =>{ 641let name =sys_name ( value) ?. to_owned (); 642if deny{ 643self . sys . deny . push ( name); 644} else { 645self . sys . allow . push ( name); 646} 647} 648Category :: Net =>{ 649let grant =NetGrant :: parse ( value) ?; 650if deny{ 651self . net . deny . push ( grant); 652} else { 653self . net . allow . push ( grant); 654} 655} 656Category :: Read |Category :: Write |Category :: JsExec |Category :: Run |Category :: Ffi =>{ 657let mut grant =if matches! ( category, Category :: Run |Category :: Ffi ) { 658PathGrant :: parse_run ( value, cwd) ? 659} else if category ==Category :: Write { 660PathGrant :: parse_file ( value, cwd) ? 661} else { 662PathGrant :: parse ( value, cwd) ? 663}; 664let policy =match category{ 665Category :: Read =>& mut self . read , 666Category :: Write =>& mut self . write , 667Category :: Run =>& mut self . run , 668Category :: Ffi =>& mut self . ffi , 669 _ =>& mut self . js_exec , 670}; 671if deny{ 672 grant. directory =true ; 673 policy. deny . push ( grant); 674} else { 675 policy. allow . push ( grant); 676} 677} 678} 679Ok (()) 680} 681 682pub fn remote_entry ( & mut self , url : & url:: Url ) ->Result <(), CapabilitiesError > { 683self . js_remote . allow . push ( RemoteGrant { 684url : canonical_remote_url ( url) ?, 685subtree : false , 686}); 687Ok (()) 688} 689 690pub fn entry ( & mut self , path : & Path ) { 691self . js_exec . allow . push ( PathGrant { 692path : path. to_owned (), 693directory : false , 694}); 695} 696} 697 698// Stored parents are immutable snapshots, never live ceilings. 699pub struct Capabilities { 700initial : Initial , 701restrictions : RwLock < Initial >, 702parent : Option < Arc < Self >>, 703cwd : PathBuf , 704} 705 706pub type Pledge =Vec <( Category , Option < Vec < String >>, Vec < String >)>; 707 708enum Restriction { 709Env ( Option < Vec < String >>, Vec < String >), 710Sys ( Option < Vec < String >>, Vec < String >), 711Net ( Option < Vec < NetGrant >>, Vec < NetGrant >), 712Read ( Option < Vec < PathGrant >>, Vec < PathGrant >), 713Write ( Option < Vec < PathGrant >>, Vec < PathGrant >), 714JsExec ( Option < Vec < PathGrant >>, Vec < PathGrant >), 715Run ( Option < Vec < PathGrant >>, Vec < PathGrant >), 716Ffi ( Option < Vec < PathGrant >>, Vec < PathGrant >), 717JsRemote ( Option < Vec < RemoteGrant >>, Vec < RemoteGrant >), 718} 719 720impl Capabilities { 721pub fn new ( initial : Initial , cwd : PathBuf ) ->Arc < Self > { 722Arc :: new ( Self { 723 initial, 724restrictions : RwLock :: new ( Initial :: default ()), 725parent : None , 726 cwd, 727}) 728} 729 730pub fn inherit ( parent : Arc < Self >) ->Arc < Self > { 731// The chain above the parent is already frozen, so cloning this policy 732// under one lock and sharing the chain is a complete atomic snapshot. 733let snapshot =Self { 734initial : parent. initial . clone (), 735restrictions : RwLock :: new ( parent. restrictions . read (). unwrap (). clone ()), 736parent : parent. parent . clone (), 737cwd : parent. cwd . clone (), 738}; 739Arc :: new ( Self { 740initial : Initial :: default (), 741restrictions : RwLock :: new ( Initial :: default ()), 742cwd : parent. cwd . clone (), 743parent : Some ( Arc :: new ( snapshot)), 744}) 745} 746 747pub fn pledge ( & self , request : Pledge ) ->Result <(), CapabilitiesError > { 748let mut prepared =Vec :: new (); 749for ( category, allow, deny) in request{ 750let restriction =match category{ 751Category :: Sys =>Restriction :: Sys ( 752 allow 753. map ( |values|{ 754 values 755. iter () 756. map ( |s|sys_name ( s). map ( str:: to_owned)) 757. collect () 758}) 759. transpose () ?, 760 deny. iter () 761. map ( |s|sys_name ( s). map ( str:: to_owned)) 762. collect ::< Result < _ , _ >>() ?, 763), 764Category :: Env =>Restriction :: Env ( 765 allow 766. map ( |values|{ 767 values 768. iter () 769. map ( |s|env_name ( s). map ( Cow :: into_owned)) 770. collect () 771}) 772. transpose () ?, 773 deny. iter () 774. map ( |s|env_name ( s). map ( Cow :: into_owned)) 775. collect ::< Result < _ , _ >>() ?, 776), 777Category :: Net =>Restriction :: Net ( 778 allow 779. map ( |values|{ 780 values 781. iter () 782. map ( |s|NetGrant :: parse ( s)) 783. collect ::< Result < Vec < _ >, _ >>() 784}) 785. transpose () ?, 786 deny. iter () 787. map ( |s|NetGrant :: parse ( s)) 788. collect ::< Result < Vec < _ >, _ >>() ?, 789), 790Category :: JsExec =>{ 791let parse = |values : Vec < String > |{ 792let mut paths =Vec :: new (); 793let mut remote =Vec :: new (); 794for valuein values{ 795if remote_execution ( & value) { 796 remote. push ( RemoteGrant :: parse ( & value) ?); 797} else { 798 paths. push ( PathGrant :: parse ( & value, & self . cwd ) ?); 799} 800} 801Ok ::< _ , CapabilitiesError >(( paths, remote)) 802}; 803let allow = allow. map ( parse). transpose () ?; 804let ( mut paths, mut remote) =parse ( deny) ?; 805for grantin & mut paths{ 806 grant. directory =true ; 807} 808for grantin & mut remote{ 809 grant. subtree =true ; 810} 811 prepared. push ( Restriction :: JsExec ( 812 allow. as_ref (). map ( |( paths, _) | paths. clone ()), 813 paths, 814)); 815Restriction :: JsRemote ( allow. map ( |( _, remote) | remote), remote) 816} 817Category :: Read |Category :: Write |Category :: Run |Category :: Ffi =>{ 818let parse = |values : Vec < String > |{ 819 values 820. iter () 821. map ( |s|{ 822if matches! ( category, Category :: Run |Category :: Ffi ) { 823PathGrant :: parse_run ( s, & self . cwd ) 824} else if category ==Category :: Write { 825PathGrant :: parse_file ( s, & self . cwd ) 826} else { 827PathGrant :: parse ( s, & self . cwd ) 828} 829}) 830. collect ::< Result < Vec < _ >, _ >>() 831}; 832let allow = allow. map ( parse). transpose () ?; 833let mut deny =parse ( deny) ?; 834for grantin & mut deny{ 835 grant. directory =true ; 836} 837match category{ 838Category :: Read =>Restriction :: Read ( allow, deny), 839Category :: Write =>Restriction :: Write ( allow, deny), 840Category :: Run =>Restriction :: Run ( allow, deny), 841Category :: Ffi =>Restriction :: Ffi ( allow, deny), 842 _ =>unreachable! (), 843} 844} 845}; 846 prepared. push ( restriction); 847} 848let mut state =self . restrictions . write (). unwrap (); 849for restrictionin prepared{ 850match restriction{ 851Restriction :: Env ( allow, deny) => state. env . restrict ( allow, deny), 852Restriction :: Sys ( allow, deny) => state. sys . restrict ( allow, deny), 853Restriction :: Net ( allow, deny) => state. net . restrict ( allow, deny), 854Restriction :: Read ( allow, deny) => state. read . restrict ( allow, deny), 855Restriction :: Write ( allow, deny) => state. write . restrict ( allow, deny), 856Restriction :: JsExec ( allow, deny) => state. js_exec . restrict ( allow, deny), 857Restriction :: Run ( allow, deny) => state. run . restrict ( allow, deny), 858Restriction :: Ffi ( allow, deny) => state. ffi . restrict ( allow, deny), 859Restriction :: JsRemote ( allow, deny) => state. js_remote . restrict ( allow, deny), 860} 861} 862Ok (()) 863} 864 865pub fn check_run ( & self , target : & PathTarget ) ->Result <(), CapabilitiesError > { 866let matches = |grant : & PathGrant | grant. matches ( target. as_path ()); 867let state =self . restrictions . read (). unwrap (); 868if self . initial . run . permits ( matches) && state. run . permits ( matches) { 869if let Some ( parent) =& self . parent { 870return parent. check_run ( target); 871} 872return Ok (()); 873} 874Err ( CapabilitiesError :: RunDenied ( target. as_path (). to_owned ())) 875} 876 877pub fn check_ffi ( & self , target : & PathTarget ) ->Result <(), CapabilitiesError > { 878let matches = |grant : & PathGrant | grant. matches ( target. as_path ()); 879let state =self . restrictions . read (). unwrap (); 880if self . initial . ffi . permits ( matches) && state. ffi . permits ( matches) { 881if let Some ( parent) =& self . parent { 882return parent. check_ffi ( target); 883} 884return Ok (()); 885} 886Err ( CapabilitiesError :: FfiDenied ( target. as_path (). to_owned ())) 887} 888 889/// Whether any ffi authority remains at all. Path grants count: holding 890/// authority to load a specific library is enough to use pointer and 891/// function-pointer operations, which name no path of their own. This 892/// looks for a witness path that every layer (initial grants, pledges, 893/// and parents) would permit, so deny rules and disjoint retain lists 894/// count as withholding authority. 895pub fn check_ffi_any ( & self ) ->Result <(), CapabilitiesError > { 896let mut policies =vec! [ self . initial . ffi . clone ()]; 897{ 898let state =self . restrictions . read (). unwrap (); 899 policies. push ( state. ffi . clone ()); 900} 901if let Some ( parent) =& self . parent { 902 parent. collect_ffi_policies ( & mut policies); 903} 904// Grant spellings cannot contain NUL, so only a root deny covers this. 905let mut witnesses =vec! [ PathBuf :: from ( "/\0" )]; 906for policyin & policies{ 907 witnesses. extend ( policy. allow . iter (). map ( |grant| grant. path . clone ())); 908 witnesses. extend ( 909 policy 910. retain 911. iter () 912. flatten () 913. map ( |grant| grant. path . clone ()), 914); 915} 916let permitted = witnesses. iter (). any ( |witness|{ 917 policies 918. iter () 919. all ( |policy| policy. permits ( |grant| grant. matches ( witness))) 920}); 921if permitted{ 922Ok (()) 923} else { 924Err ( CapabilitiesError :: FfiDeniedAny ) 925} 926} 927 928fn collect_ffi_policies ( & self , policies : & mut Vec < Policy < PathGrant >>) { 929 policies. push ( self . initial . ffi . clone ()); 930{ 931let state =self . restrictions . read (). unwrap (); 932 policies. push ( state. ffi . clone ()); 933} 934if let Some ( parent) =& self . parent { 935 parent. collect_ffi_policies ( policies); 936} 937} 938 939pub fn unrestricted_env ( & self ) ->bool { 940let state =self . restrictions . read (). unwrap (); 941self . initial . env . all 942 &&self . initial . env . deny . is_empty () 943 &&self . initial . env . retain . is_empty () 944 && state. env . deny . is_empty () 945 && state. env . retain . is_empty () 946 &&self 947. parent 948. as_ref () 949. is_none_or ( |parent| parent. unrestricted_env ()) 950} 951 952pub fn check_env ( & self , name : & str ) ->Result <(), CapabilitiesError > { 953self . check_env_name ( & env_name ( name) ?) 954} 955 956fn check_env_name ( & self , name : & str ) ->Result <(), CapabilitiesError > { 957let state =self . restrictions . read (). unwrap (); 958let matches = |grant : & String | grant == name; 959if self . initial . env . permits ( matches) && state. env . permits ( matches) { 960if let Some ( parent) =& self . parent { 961return parent. check_env_name ( name); 962} 963return Ok (()); 964} 965Err ( CapabilitiesError :: EnvDenied ( name. to_owned ())) 966} 967 968pub fn check_sys ( & self , name : & str ) ->Result <(), CapabilitiesError > { 969self . check_sys_name ( sys_name ( name) ?) 970} 971 972fn check_sys_name ( & self , name : & str ) ->Result <(), CapabilitiesError > { 973let state =self . restrictions . read (). unwrap (); 974let matches = |grant : & String | grant == name; 975if self . initial . sys . permits ( matches) && state. sys . permits ( matches) { 976if let Some ( parent) =& self . parent { 977return parent. check_sys_name ( name); 978} 979return Ok (()); 980} 981Err ( CapabilitiesError :: SysDenied ( name. to_owned ())) 982} 983 984pub fn check_net ( & self , host : & str , port : u16 ) ->Result <(), CapabilitiesError > { 985let mut target =match host. parse ::< std:: net:: Ipv6Addr >() { 986Ok ( ip) =>NetGrant { 987host : url:: Host :: Ipv6 ( ip), 988port : None , 989}, 990Err ( _) =>NetGrant :: parse ( host) ?, 991}; 992if target. port . is_some () { 993return Err ( CapabilitiesError :: NetworkRequiresHostPort ); 994} 995 target. port =Some ( port); 996if self . permits_net ( & target) { 997Ok (()) 998} else { 999Err ( CapabilitiesError :: NetDenied ( host. to_owned (), port)) 1000} 1001} 1002 1003fn permits_net ( & self , target : & NetGrant ) ->bool { 1004let state =self . restrictions . read (). unwrap (); 1005let matches = |grant : & NetGrant | grant. matches ( target); 1006self . initial . net . permits ( matches) 1007 && state. net . permits ( matches) 1008 &&self 1009. parent 1010. as_ref () 1011. is_none_or ( |parent| parent. permits_net ( target)) 1012} 1013 1014pub fn check_read ( & self , target : & PathTarget ) ->Result <(), CapabilitiesError > { 1015let path = target. as_path (); 1016let state =self . restrictions . read (). unwrap (); 1017let matches = |grant : & PathGrant | grant. matches ( path); 1018if self . initial . read . permits ( matches) && state. read . permits ( matches) { 1019if let Some ( parent) =& self . parent { 1020return parent. check_read ( target); 1021} 1022return Ok (()); 1023} 1024Err ( CapabilitiesError :: ReadDenied ( path. to_owned ())) 1025} 1026 1027pub fn cwd ( & self ) ->& Path { 1028& self . cwd 1029} 1030 1031pub fn check_write ( & self , target : & PathTarget ) ->Result <(), CapabilitiesError > { 1032let path = target. as_path (); 1033let state =self . restrictions . read (). unwrap (); 1034let matches = |grant : & PathGrant | grant. matches ( path); 1035if self . initial . write . permits ( matches) && state. write . permits ( matches) { 1036if let Some ( parent) =& self . parent { 1037return parent. check_write ( target); 1038} 1039return Ok (()); 1040} 1041Err ( CapabilitiesError :: WriteDenied ( path. to_owned ())) 1042} 1043 1044pub fn check_read_tree ( & self , target : & PathTarget ) ->Result <(), CapabilitiesError > { 1045self . check_tree ( target, false ) 1046} 1047 1048pub fn check_write_tree ( & self , target : & PathTarget ) ->Result <(), CapabilitiesError > { 1049self . check_tree ( target, true ) 1050} 1051 1052fn check_tree ( & self , target : & PathTarget , write : bool ) ->Result <(), CapabilitiesError > { 1053let root = target. as_path (); 1054let permits = |policy : & Policy < PathGrant > |{ 1055let covers = |grant : & PathGrant | grant. directory && root. starts_with ( & grant. path ); 1056( policy. all || policy. allow . iter (). any ( covers)) 1057 && policy. retain . iter (). all ( |list| list. iter (). any ( covers)) 1058 && !policy 1059. deny 1060. iter () 1061. any ( |grant| root. starts_with ( & grant. path ) || grant. path . starts_with ( root)) 1062}; 1063let state =self . restrictions . read (). unwrap (); 1064let initial =if write{ 1065& self . initial . write 1066} else { 1067& self . initial . read 1068}; 1069let restriction =if write{ & state. write } else { & state. read }; 1070if permits ( initial) 1071 &&permits ( restriction) 1072 &&self 1073. parent 1074. as_ref () 1075. is_none_or ( |parent| parent. check_tree ( target, write). is_ok ()) 1076{ 1077Ok (()) 1078} else { 1079Err ( if write{ 1080CapabilitiesError :: WriteTreeDenied ( root. to_owned ()) 1081} else { 1082CapabilitiesError :: ReadTreeDenied ( root. to_owned ()) 1083}) 1084} 1085} 1086 1087pub fn unrestricted_run ( & self ) ->bool { 1088let state =self . restrictions . read (). unwrap (); 1089self . initial . run . all 1090 &&self . initial . run . deny . is_empty () 1091 &&self . initial . run . retain . is_empty () 1092 && state. run . deny . is_empty () 1093 && state. run . retain . is_empty () 1094 &&self 1095. parent 1096. as_ref () 1097. is_none_or ( |parent| parent. unrestricted_run ()) 1098} 1099 1100pub fn unrestricted_ffi ( & self ) ->bool { 1101let state =self . restrictions . read (). unwrap (); 1102self . initial . ffi . all 1103 &&self . initial . ffi . deny . is_empty () 1104 &&self . initial . ffi . retain . is_empty () 1105 && state. ffi . deny . is_empty () 1106 && state. ffi . retain . is_empty () 1107 &&self 1108. parent 1109. as_ref () 1110. is_none_or ( |parent| parent. unrestricted_ffi ()) 1111} 1112 1113pub fn unrestricted_read ( & self ) ->bool { 1114self . unrestricted_path ( false ) 1115} 1116 1117pub fn unrestricted_write ( & self ) ->bool { 1118self . unrestricted_path ( true ) 1119} 1120 1121fn unrestricted_path ( & self , write : bool ) ->bool { 1122let state =self . restrictions . read (). unwrap (); 1123let initial =if write{ 1124& self . initial . write 1125} else { 1126& self . initial . read 1127}; 1128let restriction =if write{ & state. write } else { & state. read }; 1129 initial. all 1130 && initial. deny . is_empty () 1131 && initial. retain . is_empty () 1132 && restriction. deny . is_empty () 1133 && restriction. retain . is_empty () 1134 &&self 1135. parent 1136. as_ref () 1137. is_none_or ( |parent| parent. unrestricted_path ( write)) 1138} 1139 1140pub fn unrestricted_js_exec ( & self ) ->bool { 1141let state =self . restrictions . read (). unwrap (); 1142let unrestricted =self . initial . js_exec . deny . is_empty () 1143 &&self . initial . js_exec . retain . is_empty () 1144 && state. js_exec . deny . is_empty () 1145 && state. js_exec . retain . is_empty () 1146 &&( self . initial . js_exec . all 1147 ||( self . initial . read . all 1148 &&self . initial . read . deny . is_empty () 1149 && state. read . deny . is_empty () 1150 && state. read . retain . is_empty ())); 1151 unrestricted 1152 &&self 1153. parent 1154. as_ref () 1155. is_none_or ( |parent| parent. unrestricted_js_exec ()) 1156} 1157 1158pub fn check_remote_acquire ( & self , package : & url:: Url ) ->Result <(), CapabilitiesError > { 1159let canonical =canonical_remote_url ( package) ?; 1160let package =& canonical; 1161let state =self . restrictions . read (). unwrap (); 1162let potential = |policy : & Policy < RemoteGrant > |{ 1163( policy. all || policy. allow . iter (). any ( |grant| grant. overlaps ( package))) 1164 && !policy. deny . iter (). any ( |grant| grant. matches ( package)) 1165 && policy 1166. retain 1167. iter () 1168. all ( |list| list. iter (). any ( |grant| grant. overlaps ( package))) 1169}; 1170if potential ( & self . initial . js_remote ) &&potential ( & state. js_remote ) { 1171if let Some ( parent) =& self . parent { 1172return parent. check_remote_acquire ( package); 1173} 1174return Ok (()); 1175} 1176Err ( CapabilitiesError :: RemoteAcquireDenied ( package. to_string ())) 1177} 1178 1179pub fn check_remote_exec ( & self , target : & url:: Url ) ->Result <(), CapabilitiesError > { 1180let canonical =canonical_remote_url ( target) ?; 1181let target =& canonical; 1182let state =self . restrictions . read (). unwrap (); 1183let matches = |grant : & RemoteGrant | grant. matches ( target); 1184if self . initial . js_remote . permits ( matches) && state. js_remote . permits ( matches) { 1185if let Some ( parent) =& self . parent { 1186return parent. check_remote_exec ( target); 1187} 1188return Ok (()); 1189} 1190Err ( CapabilitiesError :: RemoteExecDenied ( target. to_string ())) 1191} 1192 1193pub fn check_js_exec ( & self , target : & PathTarget ) ->Result <(), CapabilitiesError > { 1194let path = target. as_path (); 1195let state =self . restrictions . read (). unwrap (); 1196let matches = |grant : & PathGrant | grant. matches ( path); 1197let read =self . initial . read . permits ( matches) && state. read . permits ( matches); 1198let exec =self . initial . js_exec . granted ( matches) || read; 1199if exec &&self . initial . js_exec . bounded ( matches) && state. js_exec . bounded ( matches) { 1200if let Some ( parent) =& self . parent { 1201return parent. check_js_exec ( target); 1202} 1203return Ok (()); 1204} 1205Err ( CapabilitiesError :: JsExecDenied ( path. to_owned ())) 1206} 1207}