@@ -430,25 +430,19 @@ pub(crate) fn redact_env_values_in(
430430 redact_secrets_with ( s, & values)
431431}
432432
433- /// Copy a resolved provider into a private staging directory while hashing
434- /// exactly the bytes copied. The staged file becomes non-writable before either
435- /// invocation, closing the path replacement and in-place rewrite races.
436- fn stage_provider (
433+ /// Copy a resolved provider into an unpublished candidate while hashing exactly
434+ /// the bytes copied. The caller owns the writable handle until publication.
435+ fn copy_provider_to_candidate (
437436 binary : & Path ,
438- ) -> Result < ( tempfile:: TempDir , PathBuf , String , std:: fs:: File ) , String > {
439- let directory = tempfile:: Builder :: new ( )
440- . prefix ( "buzz-provider-" )
441- . tempdir ( )
442- . map_err ( |error| format ! ( "failed to create provider staging directory: {error}" ) ) ?;
443- let suffix = if cfg ! ( windows) { ".exe" } else { "" } ;
444- let staged_path = directory. path ( ) . join ( format ! ( "provider{suffix}" ) ) ;
437+ candidate_path : & Path ,
438+ ) -> Result < ( std:: fs:: File , String ) , String > {
445439 let mut source = std:: fs:: File :: open ( binary)
446440 . map_err ( |error| format ! ( "failed to open provider for staging: {error}" ) ) ?;
447- let mut staged = std:: fs:: OpenOptions :: new ( )
441+ let mut candidate = std:: fs:: OpenOptions :: new ( )
448442 . write ( true )
449443 . create_new ( true )
450- . open ( & staged_path )
451- . map_err ( |error| format ! ( "failed to create staged provider: {error}" ) ) ?;
444+ . open ( candidate_path )
445+ . map_err ( |error| format ! ( "failed to create staged provider candidate : {error}" ) ) ?;
452446 let mut hasher = Sha256 :: new ( ) ;
453447 let mut buffer = [ 0_u8 ; 64 * 1024 ] ;
454448 loop {
@@ -458,18 +452,30 @@ fn stage_provider(
458452 if count == 0 {
459453 break ;
460454 }
461- staged
455+ candidate
462456 . write_all ( & buffer[ ..count] )
463- . map_err ( |error| format ! ( "failed to write staged provider: {error}" ) ) ?;
457+ . map_err ( |error| format ! ( "failed to write staged provider candidate : {error}" ) ) ?;
464458 hasher. update ( & buffer[ ..count] ) ;
465459 }
466- staged
460+ Ok ( ( candidate, hex:: encode ( hasher. finalize ( ) ) ) )
461+ }
462+
463+ /// Seal and close a provider candidate before atomically publishing the final
464+ /// executable pathname. Linux can reject execution with `ETXTBSY` when that
465+ /// pathname has an outstanding writer, even across a very short close/exec
466+ /// boundary, so the writable pathname is never also the executable pathname.
467+ fn publish_provider_candidate (
468+ candidate_path : & Path ,
469+ staged_path : & Path ,
470+ candidate : std:: fs:: File ,
471+ ) -> Result < ( ) , String > {
472+ candidate
467473 . sync_all ( )
468- . map_err ( |error| format ! ( "failed to sync staged provider: {error}" ) ) ?;
474+ . map_err ( |error| format ! ( "failed to sync staged provider candidate : {error}" ) ) ?;
469475
470- let mut permissions = staged
476+ let mut permissions = candidate
471477 . metadata ( )
472- . map_err ( |error| format ! ( "failed to inspect staged provider: {error}" ) ) ?
478+ . map_err ( |error| format ! ( "failed to inspect staged provider candidate : {error}" ) ) ?
473479 . permissions ( ) ;
474480 #[ cfg( unix) ]
475481 {
@@ -478,9 +484,29 @@ fn stage_provider(
478484 }
479485 #[ cfg( not( unix) ) ]
480486 permissions. set_readonly ( true ) ;
481- std:: fs:: set_permissions ( & staged_path, permissions)
482- . map_err ( |error| format ! ( "failed to protect staged provider: {error}" ) ) ?;
483- drop ( staged) ;
487+ std:: fs:: set_permissions ( candidate_path, permissions)
488+ . map_err ( |error| format ! ( "failed to protect staged provider candidate: {error}" ) ) ?;
489+ drop ( candidate) ;
490+ std:: fs:: rename ( candidate_path, staged_path)
491+ . map_err ( |error| format ! ( "failed to publish staged provider atomically: {error}" ) ) ?;
492+ Ok ( ( ) )
493+ }
494+
495+ /// Stage one immutable provider executable in a private directory. The final
496+ /// pathname is published only after the candidate is synced, protected, and
497+ /// closed; a read-only guard then keeps that inode alive across both calls.
498+ fn stage_provider (
499+ binary : & Path ,
500+ ) -> Result < ( tempfile:: TempDir , PathBuf , String , std:: fs:: File ) , String > {
501+ let directory = tempfile:: Builder :: new ( )
502+ . prefix ( "buzz-provider-" )
503+ . tempdir ( )
504+ . map_err ( |error| format ! ( "failed to create provider staging directory: {error}" ) ) ?;
505+ let suffix = if cfg ! ( windows) { ".exe" } else { "" } ;
506+ let staged_path = directory. path ( ) . join ( format ! ( "provider{suffix}" ) ) ;
507+ let candidate_path = directory. path ( ) . join ( format ! ( ".provider{suffix}.partial" ) ) ;
508+ let ( candidate, digest) = copy_provider_to_candidate ( binary, & candidate_path) ?;
509+ publish_provider_candidate ( & candidate_path, & staged_path, candidate) ?;
484510
485511 #[ cfg( windows) ]
486512 let execution_guard = {
@@ -496,12 +522,7 @@ fn stage_provider(
496522 let execution_guard = std:: fs:: File :: open ( & staged_path) ;
497523 let execution_guard = execution_guard
498524 . map_err ( |error| format ! ( "failed to lock staged provider for execution: {error}" ) ) ?;
499- Ok ( (
500- directory,
501- staged_path,
502- hex:: encode ( hasher. finalize ( ) ) ,
503- execution_guard,
504- ) )
525+ Ok ( ( directory, staged_path, digest, execution_guard) )
505526}
506527
507528/// Deploy through one immutable staged copy: negotiate protocol v1 before the
0 commit comments