char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f4a0f5b
main
1use super :: paths::{ follows_link, mutable_entry, resolve}; 2use super :: * ; 3 4# [ cfg ( unix )] 5use std:: os:: fd:: AsFd ; 6 7# [ cfg ( target_os = "linux" )] 8use std:: os:: fd:: AsRawFd ; 9 10# [ cfg ( unix )] 11fn times ( secs : i64 , nanos : u32 ) -> io:: Result < rustix:: fs:: Timespec > { 12if !( -8_640_000_000_000 ..=8_640_000_000_000 ). contains ( & secs) 13 ||( secs ==8_640_000_000_000 && nanos >0 ) 14 || nanos >=1_000_000_000 15{ 16return Err ( io:: Error :: new ( 17 io:: ErrorKind :: InvalidInput , 18"Invalid timestamp" , 19)); 20} 21Ok ( rustix:: fs:: Timespec { 22tv_sec : secs, 23tv_nsec : nanosas _ , 24}) 25} 26 27fn unchanged ( directory : & Dir , name : & std:: ffi:: OsStr ) -> io:: Result <()> { 28// Following ops resolve the referent before checking authority, so the 29// leaf must still be that referent at use time; anything else raced. 30if directory. symlink_metadata ( name) ?. is_symlink () { 31return Err ( io:: Error :: new ( 32 io:: ErrorKind :: NotFound , 33"Symbolic link changed or has a missing target" , 34)); 35} 36Ok (()) 37} 38 39fn link_changed ( error : io:: Error ) -> io:: Error { 40// A loop in the resolved path names no file; report it like a raced link. 41# [ cfg ( unix )] 42if error. raw_os_error () ==Some ( libc:: ELOOP ) { 43return io:: Error :: new ( 44 io:: ErrorKind :: NotFound , 45"Symbolic link changed or has a missing target" , 46); 47} 48 error 49} 50 51# [ cfg ( target_os = "linux" )] 52fn fchmodat2_nr () ->Option < libc:: c_long > { 53// fchmodat2 is number 452 on the shared-table architectures and x86; 54// x32 offsets it, and alpha/mips number it differently or not at all. 55# [ cfg ( all ( target_arch = "x86_64" , target_pointer_width = "32" ))] 56{ 57Some ( libc:: SYS_fchmodat2 ) 58} 59# [ cfg ( any ( 60all ( target_arch = "x86_64" , target_pointer_width = "64" ), 61target_arch = "x86" , 62target_arch = "aarch64" , 63target_arch = "arm" , 64target_arch = "riscv64" , 65target_arch = "riscv32" , 66target_arch = "loongarch64" , 67target_arch = "m68k" , 68target_arch = "csky" , 69target_arch = "powerpc" , 70target_arch = "powerpc64" , 71target_arch = "s390x" , 72target_arch = "sparc" , 73target_arch = "sparc64" 74))] 75{ 76Some ( 452 ) 77} 78# [ cfg ( not ( any ( 79target_arch = "x86_64" , 80target_arch = "x86" , 81target_arch = "aarch64" , 82target_arch = "arm" , 83target_arch = "riscv64" , 84target_arch = "riscv32" , 85target_arch = "loongarch64" , 86target_arch = "m68k" , 87target_arch = "csky" , 88target_arch = "powerpc" , 89target_arch = "powerpc64" , 90target_arch = "s390x" , 91target_arch = "sparc" , 92target_arch = "sparc64" 93)))] 94{ 95None 96} 97} 98 99# [ cfg ( target_os = "linux" )] 100fn chmod_nofollow ( 101directory : std:: os:: fd:: BorrowedFd < ' _ >, 102name : & std:: ffi:: OsStr , 103mode : u32 , 104) -> io:: Result <()> { 105use std:: os:: unix:: ffi:: OsStrExt ; 106let name = std:: ffi:: CString :: new ( name. as_bytes ()) 107. map_err ( |_| io:: Error :: new ( io:: ErrorKind :: InvalidInput , "Invalid file name" )) ?; 108// SAFETY: fchmodat2 takes a directory fd, a NUL-terminated leaf name, 109// a mode, and flags; all are valid here. 110let number =match fchmodat2_nr () { 111// Architectures numbering fchmodat2 differently take the open path. 112Some ( number) => number, 113None =>return Err ( io:: Error :: from_raw_os_error ( libc:: ENOSYS )), 114}; 115let result =unsafe { 116 libc:: syscall ( 117 number, 118 directory. as_raw_fd (), 119 name. as_ptr (), 120 mode, 121 libc:: AT_SYMLINK_NOFOLLOW , 122) 123}; 124if result ==0 { 125Ok (()) 126} else { 127Err ( io:: Error :: last_os_error ()) 128} 129} 130 131# [ cfg ( unix )] 132fn owner ( 133uid : Option < u32 >, 134gid : Option < u32 >, 135) -> io:: Result <( Option < rustix:: fs:: Uid >, Option < rustix:: fs:: Gid >)> { 136// -1 (u32::MAX) means "no change" to the kernel; it must stay explicit null. 137if uid. is_some_and ( |id| id == u32:: MAX ) || gid. is_some_and ( |id| id == u32:: MAX ) { 138return Err ( io:: Error :: new ( 139 io:: ErrorKind :: InvalidInput , 140"Invalid user or group id" , 141)); 142} 143if uid. is_none () && gid. is_none () { 144return Err ( io:: Error :: new ( 145 io:: ErrorKind :: InvalidInput , 146"Expected a user or group id" , 147)); 148} 149Ok (( 150 uid. map ( rustix:: fs:: Uid :: from_raw), 151 gid. map ( rustix:: fs:: Gid :: from_raw), 152)) 153} 154 155pub ( super ) fn ops ( files : Arc < Files >) ->Vec < Op > { 156let ops = files. clone (); 157let chmod = files. clone (); 158let chown = files. clone (); 159let lchown = files. clone (); 160let utime = files. clone (); 161let lutime = files. clone (); 162let link = files. clone (); 163let truncate = files. clone (); 164let fchmod = files. clone (); 165let fchown = files. clone (); 166let futime = files. clone (); 167let flock = files. clone (); 168let funlock = files. clone (); 169vec! [ 170 ops. io_ops ( 171"op_file_chmod" , 172"op_file_chmod_sync" , 173move |( path, mode): ( String , u32 ) |{ 174let caps = chmod. caps . clone (); 175let pending = chmod. pool . run ( move ||{ 176if mode >0o7777 { 177return Err ( io:: Error :: new ( 178 io:: ErrorKind :: InvalidInput , 179"Invalid file mode" , 180)); 181} 182let target =resolve ( & caps, & path, false , true ). map_err ( link_changed) ?; 183let ( directory, name) =mutable_entry ( & target) ?; 184authorize ( & caps, & target, & path, true ) ?; 185unchanged ( & directory, & name) ?; 186# [ cfg ( target_os = "linux" )] 187{ 188// fchmodat2 with NOFOLLOW chmods the resolved leaf itself: a 189// link swapped in after the pre-check fails closed instead 190// of retargeting, and with no open, unreadable files, 191// locked directories, and device nodes need no access. 192match chmod_nofollow ( directory. as_fd (), & name, mode) { 193Ok (()) =>Ok ( true ), 194Err ( error) if error. raw_os_error () ==Some ( libc:: EOPNOTSUPP ) =>{ 195// A link swapped in after the pre-check fails here 196// instead of retargeting. 197Err ( io:: Error :: new ( 198 io:: ErrorKind :: NotFound , 199"Symbolic link changed or has a missing target" , 200)) 201} 202Err ( error) 203if matches! ( 204 error. raw_os_error (), 205Some ( libc:: ENOSYS ) |Some ( libc:: EPERM ) 206) => 207{ 208// Kernels before 6.6 lack fchmodat2, and seccomp 209// filters may block it; open without following, 210// preferring read over write. 211let mut read =OpenOptions :: new (); 212 read. read ( true ). follow ( FollowSymlinks :: No ). nonblock ( true ); 213let mut write =OpenOptions :: new (); 214 write. write ( true ). follow ( FollowSymlinks :: No ). nonblock ( true ); 215let opened = directory 216. open_with ( & name, & read) 217. or_else ( |_| directory. open_with ( & name, & write)) 218. map_err ( link_changed) ?; 219let metadata = opened. metadata () ?; 220if !metadata. is_file () && !metadata. is_dir () { 221return Err ( io:: Error :: new ( 222 io:: ErrorKind :: InvalidInput , 223"Cannot change mode of non-regular file" , 224)); 225} 226 rustix:: fs:: fchmod ( 227 opened. as_fd (), 228 rustix:: fs:: Mode :: from_raw_mode ( mode), 229) ?; 230Ok ( true ) 231} 232Err ( error) =>Err ( error), 233} 234} 235# [ cfg ( all ( unix , not ( target_os = "linux" )))] 236{ 237 rustix:: fs:: chmodat ( 238 directory. as_fd (), 239Path :: new ( & name), 240 rustix:: fs:: Mode :: from_raw_mode ( mode), 241 rustix:: fs:: AtFlags :: SYMLINK_NOFOLLOW , 242) ?; 243Ok ( true ) 244} 245# [ cfg ( not ( unix ))] 246{ 247let _ =( & directory, & name); 248Err ( io:: Error :: new ( 249 io:: ErrorKind :: Unsupported , 250"not yet implemented: file modes on this platform" , 251)) 252} 253}); 254async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 255}, 256), 257 ops. io_ops ( 258"op_file_chown" , 259"op_file_chown_sync" , 260move |( path, uid, gid): ( String , Option < u32 >, Option < u32 >) |{ 261let caps = chown. caps . clone (); 262let pending = chown. pool . run ( move ||{ 263# [ cfg ( unix )] 264let owned =owner ( uid, gid) ?; 265let target =resolve ( & caps, & path, false , true ) ?; 266let ( directory, name) =mutable_entry ( & target) ?; 267authorize ( & caps, & target, & path, true ) ?; 268unchanged ( & directory, & name) ?; 269# [ cfg ( unix )] 270{ 271let ( uid, gid) = owned; 272 rustix:: fs:: chownat ( 273 directory. as_fd (), 274Path :: new ( & name), 275 uid, 276 gid, 277 rustix:: fs:: AtFlags :: SYMLINK_NOFOLLOW , 278) ?; 279Ok ( true ) 280} 281# [ cfg ( not ( unix ))] 282{ 283let _ =( & directory, & name, uid, gid); 284Err ( io:: Error :: new ( 285 io:: ErrorKind :: Unsupported , 286"not yet implemented: file ownership on this platform" , 287)) 288} 289}); 290async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 291}, 292), 293 ops. io_ops ( 294"op_file_lchown" , 295"op_file_lchown_sync" , 296move |( path, uid, gid): ( String , Option < u32 >, Option < u32 >) |{ 297let caps = lchown. caps . clone (); 298let pending = lchown. pool . run ( move ||{ 299# [ cfg ( unix )] 300let owned =owner ( uid, gid) ?; 301// A trailing slash names the referent, like path_stat. 302let target =resolve ( & caps, & path, !follows_link ( & path), true ) ?; 303let ( directory, name) =mutable_entry ( & target) ?; 304authorize ( & caps, & target, & path, true ) ?; 305# [ cfg ( unix )] 306{ 307let ( uid, gid) = owned; 308 rustix:: fs:: chownat ( 309 directory. as_fd (), 310Path :: new ( & name), 311 uid, 312 gid, 313 rustix:: fs:: AtFlags :: SYMLINK_NOFOLLOW , 314) ?; 315Ok ( true ) 316} 317# [ cfg ( not ( unix ))] 318{ 319let _ =( & directory, & name, uid, gid); 320Err ( io:: Error :: new ( 321 io:: ErrorKind :: Unsupported , 322"not yet implemented: file ownership on this platform" , 323)) 324} 325}); 326async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 327}, 328), 329 ops. io_ops ( 330"op_file_utime" , 331"op_file_utime_sync" , 332move |( path, asec, anano, msec, mnano): ( String , i64 , u32 , i64 , u32 ) |{ 333let caps = utime. caps . clone (); 334let pending = utime. pool . run ( move ||{ 335# [ cfg ( unix )] 336let times = rustix:: fs:: Timestamps { 337last_access : times ( asec, anano) ?, 338last_modification : times ( msec, mnano) ?, 339}; 340let target =resolve ( & caps, & path, false , true ) ?; 341let ( directory, name) =mutable_entry ( & target) ?; 342authorize ( & caps, & target, & path, true ) ?; 343unchanged ( & directory, & name) ?; 344# [ cfg ( unix )] 345{ 346 rustix:: fs:: utimensat ( 347 directory. as_fd (), 348Path :: new ( & name), 349& times, 350 rustix:: fs:: AtFlags :: SYMLINK_NOFOLLOW , 351) ?; 352Ok ( true ) 353} 354# [ cfg ( not ( unix ))] 355{ 356let _ =( & directory, & name, asec, anano, msec, mnano); 357Err ( io:: Error :: new ( 358 io:: ErrorKind :: Unsupported , 359"not yet implemented: file times on this platform" , 360)) 361} 362}); 363async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 364}, 365), 366 ops. io_ops ( 367"op_file_lutime" , 368"op_file_lutime_sync" , 369move |( path, asec, anano, msec, mnano): ( String , i64 , u32 , i64 , u32 ) |{ 370let caps = lutime. caps . clone (); 371let pending = lutime. pool . run ( move ||{ 372# [ cfg ( unix )] 373let times = rustix:: fs:: Timestamps { 374last_access : times ( asec, anano) ?, 375last_modification : times ( msec, mnano) ?, 376}; 377// A trailing slash names the referent, like path_stat. 378let target =resolve ( & caps, & path, !follows_link ( & path), true ) ?; 379let ( directory, name) =mutable_entry ( & target) ?; 380authorize ( & caps, & target, & path, true ) ?; 381# [ cfg ( unix )] 382{ 383 rustix:: fs:: utimensat ( 384 directory. as_fd (), 385Path :: new ( & name), 386& times, 387 rustix:: fs:: AtFlags :: SYMLINK_NOFOLLOW , 388) ?; 389Ok ( true ) 390} 391# [ cfg ( not ( unix ))] 392{ 393let _ =( & directory, & name, asec, anano, msec, mnano); 394Err ( io:: Error :: new ( 395 io:: ErrorKind :: Unsupported , 396"not yet implemented: file times on this platform" , 397)) 398} 399}); 400async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 401}, 402), 403 ops. io_ops ( 404"op_file_link" , 405"op_file_link_sync" , 406move |( source, destination): ( String , String ) |{ 407let caps = link. caps . clone (); 408let pending = link. pool . run ( move ||{ 409// Link authorization must not race another managed link mutation. 410let _guard =super :: links:: MUTATIONS 411. lock () 412. unwrap_or_else ( std:: sync:: PoisonError :: into_inner); 413let from =resolve ( & caps, & source, true , false ) ?; 414let to =resolve ( & caps, & destination, true , true ) ?; 415let ( from_dir, from_name) =parent ( from. as_path ()) ?; 416let ( to_dir, to_name) =mutable_entry ( & to) ?; 417authorize ( & caps, & from, & source, false ) ?; 418authorize ( & caps, & from, & source, true ) ?; 419authorize ( & caps, & to, & destination, true ) ?; 420// linkat would hard-link the link itself past its target checks. 421if from_dir. symlink_metadata ( & from_name) ?. is_symlink () { 422return Err ( io:: Error :: new ( 423 io:: ErrorKind :: InvalidInput , 424"Source is a symbolic link" , 425)); 426} 427 from_dir. hard_link ( from_name, & to_dir, to_name) ?; 428Ok ( true ) 429}); 430async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 431}, 432), 433 ops. io_ops ( 434"op_file_path_truncate" , 435"op_file_path_truncate_sync" , 436move |( path, length): ( String , u64 ) |{ 437let caps = truncate. caps . clone (); 438let pending = truncate. pool . run ( move ||{ 439if length >SAFE_INTEGER { 440return Err ( io:: Error :: new ( 441 io:: ErrorKind :: InvalidInput , 442"Unsafe truncate length" , 443)); 444} 445let target =resolve ( & caps, & path, false , true ). map_err ( link_changed) ?; 446let ( directory, name) =mutable_entry ( & target) ?; 447authorize ( & caps, & target, & path, true ) ?; 448unchanged ( & directory, & name) ?; 449let mut flags =OpenOptions :: new (); 450 flags. write ( true ). follow ( FollowSymlinks :: No ). nonblock ( true ); 451let opened = directory. open_with ( name, & flags) ?; 452if !opened. metadata () ?. is_file () { 453return Err ( io:: Error :: new ( 454 io:: ErrorKind :: InvalidInput , 455"Cannot truncate non-regular file" , 456)); 457} 458 opened. set_len ( length) ?; 459Ok ( true ) 460}); 461async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 462}, 463), 464 ops. io_ops ( 465"op_file_fchmod" , 466"op_file_fchmod_sync" , 467move |( id, mode): ( u64 , u32 ) |{ 468let pending = fchmod. submit ( id, move |opened|{ 469if mode >0o7777 { 470return Err ( io:: Error :: new ( 471 io:: ErrorKind :: InvalidInput , 472"Invalid file mode" , 473)); 474} 475 opened. check ( true ) ?; 476# [ cfg ( unix )] 477{ 478 rustix:: fs:: fchmod ( 479 opened. file . as_fd (), 480 rustix:: fs:: Mode :: from_raw_mode ( mode), 481) ?; 482Ok ( true ) 483} 484# [ cfg ( not ( unix ))] 485{ 486Err ( io:: Error :: new ( 487 io:: ErrorKind :: Unsupported , 488"not yet implemented: file modes on this platform" , 489)) 490} 491}); 492async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 493}, 494), 495 ops. io_ops ( 496"op_file_fchown" , 497"op_file_fchown_sync" , 498move |( id, uid, gid): ( u64 , Option < u32 >, Option < u32 >) |{ 499let pending = fchown. submit ( id, move |opened|{ 500# [ cfg ( unix )] 501let owned =owner ( uid, gid) ?; 502 opened. check ( true ) ?; 503# [ cfg ( unix )] 504{ 505let ( uid, gid) = owned; 506 rustix:: fs:: fchown ( opened. file . as_fd (), uid, gid) ?; 507Ok ( true ) 508} 509# [ cfg ( not ( unix ))] 510{ 511let _ =( uid, gid); 512Err ( io:: Error :: new ( 513 io:: ErrorKind :: Unsupported , 514"not yet implemented: file ownership on this platform" , 515)) 516} 517}); 518async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 519}, 520), 521 ops. io_ops ( 522"op_file_futime" , 523"op_file_futime_sync" , 524move |( id, asec, anano, msec, mnano): ( u64 , i64 , u32 , i64 , u32 ) |{ 525let pending = futime. submit ( id, move |opened|{ 526# [ cfg ( unix )] 527let times = rustix:: fs:: Timestamps { 528last_access : times ( asec, anano) ?, 529last_modification : times ( msec, mnano) ?, 530}; 531 opened. check ( true ) ?; 532# [ cfg ( unix )] 533{ 534 rustix:: fs:: futimens ( opened. file . as_fd (), & times) ?; 535Ok ( true ) 536} 537# [ cfg ( not ( unix ))] 538{ 539let _ =( asec, anano, msec, mnano); 540Err ( io:: Error :: new ( 541 io:: ErrorKind :: Unsupported , 542"not yet implemented: file times on this platform" , 543)) 544} 545}); 546async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 547}, 548), 549 ops. io_ops ( 550"op_file_flock" , 551"op_file_flock_sync" , 552move |( id, exclusive): ( u64 , bool ) |{ 553let state = flock. clone (); 554async move { 555# [ cfg ( unix )] 556{ 557// Only nonblocking attempts run on the handle queue, so a 558// waiting lock holds neither a pool thread nor the queue. 559let mut wait = std:: time:: Duration :: from_millis ( 5 ); 560loop { 561let pending = state. submit ( id, move |opened|{ 562if exclusive{ 563 opened. check ( true ) ?; 564} else { 565 opened. check ( false ) ?; 566} 567 rustix:: fs:: flock ( 568 opened. file . as_fd (), 569if exclusive{ 570 rustix:: fs:: FlockOperation :: NonBlockingLockExclusive 571} else { 572 rustix:: fs:: FlockOperation :: NonBlockingLockShared 573}, 574) ?; 575Ok ( true ) 576}); 577match receive ( pending). await { 578( value @Some ( _), None ) =>{ 579// A closed handle releases its locks; never 580// report success past close. 581let live = state. submit ( id, |_|Ok ::< _ , io:: Error >( true )); 582match receive ( live). await { 583( Some ( _), None ) =>{ 584return Ok ::< _ , Infallible >(( value, None )); 585} 586( _, error) =>return Ok (( None , error)), 587} 588} 589( None , Some (( "Busy" , _))) =>{ 590let delay ={ 591let _enter = state. pool . runtime . enter (); 592 tokio:: time:: sleep ( wait) 593}; 594 delay. await ; 595 wait =( wait* 2 ). min ( std:: time:: Duration :: from_millis ( 100 )); 596} 597 done =>return Ok ( done), 598} 599} 600} 601# [ cfg ( not ( unix ))] 602{ 603let pending = state. submit ( id, move |opened|{ 604if exclusive{ 605 opened. check ( true ) ?; 606} else { 607 opened. check ( false ) ?; 608} 609let _ = exclusive; 610Err ::< bool , _ >( io:: Error :: new ( 611 io:: ErrorKind :: Unsupported , 612"not yet implemented: file locking on this platform" , 613)) 614}); 615Ok ( receive ( pending). await ) 616} 617} 618}, 619), 620 ops. io_ops ( "op_file_funlock" , "op_file_funlock_sync" , move |id : u64 |{ 621// Releasing a lock grants nothing, so any live handle may unlock. 622let pending = funlock. submit ( id, |opened|{ 623# [ cfg ( unix )] 624{ 625 rustix:: fs:: flock ( opened. file . as_fd (), rustix:: fs:: FlockOperation :: Unlock ) ?; 626Ok ( true ) 627} 628# [ cfg ( not ( unix ))] 629{ 630let _ = opened; 631Err ( io:: Error :: new ( 632 io:: ErrorKind :: Unsupported , 633"not yet implemented: file locking on this platform" , 634)) 635} 636}); 637async move { Ok ::< _ , Infallible >( receive ( pending). await ) } 638}), 639] 640. into_iter () 641. flatten () 642. collect () 643} 644 645# [ cfg ( all ( test , unix ))] 646mod tests{ 647use super :: * ; 648 649# [ test ] 650fn leaf_check_rejects_links_swapped_in_after_resolve () { 651let root = tempfile:: tempdir (). unwrap (); 652 std:: fs:: write ( root. path (). join ( "leaf" ), b"data" ). unwrap (); 653let caps =Capabilities :: new ( avril_caps:: Initial :: new ( false ), root. path (). to_owned ()); 654let target =resolve ( & caps, "leaf" , false , true ). unwrap (); 655let ( directory, name) =parent ( target. as_path ()). unwrap (); 656unchanged ( & directory, & name). unwrap (); 657 std:: fs:: remove_file ( root. path (). join ( "leaf" )). unwrap (); 658 std:: os:: unix:: fs:: symlink ( "elsewhere" , root. path (). join ( "leaf" )). unwrap (); 659let error =unchanged ( & directory, & name). unwrap_err (); 660assert_eq! ( error. kind (), io:: ErrorKind :: NotFound ); 661assert_eq! ( 662 error. to_string (), 663"Symbolic link changed or has a missing target" 664); 665} 666}