@@ -13,11 +13,12 @@ use std::collections::HashMap;
1313use std:: env;
1414use std:: ffi:: { CString , OsString } ;
1515use std:: fs:: File as StdFile ;
16- use std:: io;
16+ use std:: io:: { self , Write } ;
1717use std:: net:: ToSocketAddrs ;
1818use std:: os:: unix:: ffi:: OsStrExt ;
1919use std:: os:: unix:: io:: { AsRawFd , FromRawFd } ;
2020use std:: os:: unix:: prelude:: PermissionsExt ;
21+ use std:: path:: { Path , PathBuf } ;
2122use std:: pin:: Pin ;
2223use std:: process:: { ExitStatus , Stdio } ;
2324use std:: sync:: Arc ;
@@ -50,6 +51,11 @@ struct RptyCommandConfig {
5051 ps1 : & ' static str ,
5152}
5253
54+ enum RptyBash {
55+ Path ( String ) ,
56+ Memfd ( StdFile ) ,
57+ }
58+
5359impl RemotePtyShell {
5460 pub ( super ) async fn new (
5561 term : & str ,
@@ -58,7 +64,7 @@ impl RemotePtyShell {
5864 ) -> Result < Self > {
5965 info ! ( "creating remote pty shell" ) ;
6066
61- let path = download_rpty_bash ( ) . await ?;
67+ let rpty_bash = download_rpty_bash ( ) . await ?;
6268 let ( sock_path, sock_listener) = create_pty_sock ( ) . await ?;
6369
6470 let command_config = RptyCommandConfig {
@@ -71,7 +77,7 @@ impl RemotePtyShell {
7177 } ,
7278 } ;
7379
74- let proc = spawn_rpty_bash ( & path , & command_config) . await ?;
80+ let proc = spawn_rpty_bash ( rpty_bash , & command_config) . await ?;
7581
7682 let ( read_tx, read_rx) = unbounded_channel ( ) ;
7783
@@ -278,19 +284,23 @@ impl Shell for RemotePtyShell {
278284 }
279285}
280286
281- async fn spawn_rpty_bash ( path : & str , config : & RptyCommandConfig ) -> Result < Child > {
282- match create_rpty_command ( path, config) . spawn ( ) {
283- Ok ( proc) => Ok ( proc) ,
284- Err ( err) if err. kind ( ) == io:: ErrorKind :: PermissionDenied => {
285- warn ! (
286- "failed to execute rpty bash from {}; retrying through memfd" ,
287- path
288- ) ;
289- spawn_rpty_bash_memfd ( path, config)
290- . await
291- . with_context ( || "Failed to start shell through memfd" )
292- }
293- Err ( err) => Err ( Error :: from ( err) . context ( "Failed to start shell" ) ) ,
287+ async fn spawn_rpty_bash ( rpty_bash : RptyBash , config : & RptyCommandConfig ) -> Result < Child > {
288+ match rpty_bash {
289+ RptyBash :: Path ( path) => match create_rpty_command ( & path, config) . spawn ( ) {
290+ Ok ( proc) => Ok ( proc) ,
291+ Err ( err) if err. kind ( ) == io:: ErrorKind :: PermissionDenied => {
292+ warn ! (
293+ "failed to execute rpty bash from {}; retrying through memfd" ,
294+ path
295+ ) ;
296+ spawn_rpty_bash_memfd ( & path, config)
297+ . await
298+ . with_context ( || "Failed to start shell through memfd" )
299+ }
300+ Err ( err) => Err ( Error :: from ( err) . context ( "Failed to start shell" ) ) ,
301+ } ,
302+ RptyBash :: Memfd ( memfd) => spawn_rpty_bash_memfd_file ( memfd, config)
303+ . with_context ( || "Failed to start shell through downloaded memfd" ) ,
294304 }
295305}
296306
@@ -320,6 +330,10 @@ async fn spawn_rpty_bash_memfd(path: &str, config: &RptyCommandConfig) -> Result
320330 . await
321331 . context ( "failed to join memfd copy task" ) ??;
322332
333+ spawn_rpty_bash_memfd_file ( memfd, config)
334+ }
335+
336+ fn spawn_rpty_bash_memfd_file ( memfd : StdFile , config : & RptyCommandConfig ) -> Result < Child > {
323337 let exec_payload = MemfdExecPayload :: new ( memfd, config) ?;
324338 let mut command = Command :: new ( "/proc/self/exe" ) ;
325339 command
@@ -338,14 +352,27 @@ fn copy_file_to_memfd(path: &str) -> io::Result<StdFile> {
338352 let mut source = StdFile :: open ( path) ?;
339353 let mut memfd = create_memfd ( ) ?;
340354 io:: copy ( & mut source, & mut memfd) ?;
355+ make_memfd_executable ( & memfd) ?;
341356
357+ Ok ( memfd)
358+ }
359+
360+ fn copy_bytes_to_memfd ( bytes : & [ u8 ] ) -> io:: Result < StdFile > {
361+ let mut memfd = create_memfd ( ) ?;
362+ memfd. write_all ( bytes) ?;
363+ make_memfd_executable ( & memfd) ?;
364+
365+ Ok ( memfd)
366+ }
367+
368+ fn make_memfd_executable ( memfd : & StdFile ) -> io:: Result < ( ) > {
342369 let mode = 0o700 ;
343370 let chmod_res = unsafe { libc:: fchmod ( memfd. as_raw_fd ( ) , mode) } ;
344371 if chmod_res == -1 {
345372 return Err ( io:: Error :: last_os_error ( ) ) ;
346373 }
347374
348- Ok ( memfd )
375+ Ok ( ( ) )
349376}
350377
351378fn create_memfd ( ) -> io:: Result < StdFile > {
@@ -457,18 +484,7 @@ fn rpty_env(config: &RptyCommandConfig) -> Result<Vec<CString>> {
457484 . collect ( )
458485}
459486
460- async fn download_rpty_bash ( ) -> Result < String > {
461- let tmp_dir = get_temp_dir ( ) . await ?;
462- let local_path = format ! ( "{tmp_dir}/bash-rpty" ) ;
463-
464- let mut file = fs:: OpenOptions :: new ( )
465- . write ( true )
466- . truncate ( true )
467- . create ( true )
468- . open ( local_path. clone ( ) )
469- . await
470- . map_err ( |_| Error :: msg ( "failed to open file" ) ) ?;
471-
487+ async fn download_rpty_bash ( ) -> Result < RptyBash > {
472488 let arch = if cfg ! ( target_arch = "x86_64" ) {
473489 "x86_64"
474490 } else if cfg ! ( target_arch = "aarch64" ) {
@@ -537,18 +553,17 @@ async fn download_rpty_bash() -> Result<String> {
537553 return Err ( Error :: msg ( "failed to find content-length from response" ) ) ;
538554 }
539555
540- debug ! ( "saving file to tmp path " ) ;
556+ debug ! ( "reading rpty bash artifact " ) ;
541557 let mut buff = [ 0u8 ; 1024 ] ;
558+ let mut body = Vec :: with_capacity ( content_length as usize ) ;
542559 let mut downloaded = 0 ;
543560 while downloaded < content_length {
544561 let n = tls. read ( & mut buff) . await . context ( "failed to read" ) ?;
545562 if n == 0 {
546563 break ;
547564 }
548565
549- file. write_all ( & buff[ ..n] )
550- . await
551- . context ( "failed to write" ) ?;
566+ body. extend_from_slice ( & buff[ ..n] ) ;
552567 downloaded += n as u64 ;
553568 }
554569
@@ -560,11 +575,52 @@ async fn download_rpty_bash() -> Result<String> {
560575
561576 debug ! ( "finished downloading file" ) ;
562577
578+ let local_path = get_exe_dir ( )
579+ . map ( |tmp_dir| format ! ( "{tmp_dir}/bash-rpty" ) )
580+ . ok ( ) ;
581+
582+ if let Some ( local_path) = local_path {
583+ match save_rpty_bash_to_file ( & local_path, & body) . await {
584+ Ok ( ( ) ) => return Ok ( RptyBash :: Path ( local_path) ) ,
585+ Err ( err) => {
586+ warn ! (
587+ "failed to save rpty bash to {}; retrying download through memfd: {}" ,
588+ local_path, err
589+ ) ;
590+ }
591+ }
592+ } else {
593+ warn ! ( "failed to find rpty bash disk cache directory; retrying download through memfd" ) ;
594+ }
595+
596+ let memfd = tokio:: task:: spawn_blocking ( move || copy_bytes_to_memfd ( & body) )
597+ . await
598+ . context ( "failed to join memfd write task" ) ?
599+ . context ( "failed to write downloaded rpty bash to memfd" ) ?;
600+
601+ Ok ( RptyBash :: Memfd ( memfd) )
602+ }
603+
604+ async fn save_rpty_bash_to_file ( local_path : & str , body : & [ u8 ] ) -> Result < ( ) > {
605+ debug ! ( "saving rpty bash to {}" , local_path) ;
606+ let mut file = fs:: OpenOptions :: new ( )
607+ . write ( true )
608+ . truncate ( true )
609+ . create ( true )
610+ . open ( local_path)
611+ . await
612+ . with_context ( || format ! ( "failed to open rpty download file {local_path}" ) ) ?;
613+
614+ file. write_all ( body)
615+ . await
616+ . with_context ( || format ! ( "failed to write rpty download file {local_path}" ) ) ?;
617+
563618 debug ! ( "making executable" ) ;
564- std:: fs:: set_permissions ( local_path. clone ( ) , std:: fs:: Permissions :: from_mode ( 0o744 ) ) . unwrap ( ) ;
619+ std:: fs:: set_permissions ( local_path, std:: fs:: Permissions :: from_mode ( 0o744 ) )
620+ . with_context ( || format ! ( "failed to chmod rpty download file {local_path}" ) ) ?;
565621 debug ! ( "done" ) ;
566622
567- Ok ( local_path )
623+ Ok ( ( ) )
568624}
569625
570626async fn read_line ( tls : & mut TlsStream < TcpStream > ) -> Result < String > {
@@ -590,17 +646,78 @@ async fn read_line(tls: &mut TlsStream<TcpStream>) -> Result<String> {
590646}
591647
592648async fn get_temp_dir ( ) -> Result < String > {
649+ for dir in temp_dir_candidates ( ) {
650+ match ensure_writable_dir ( & dir) {
651+ Ok ( ( ) ) => {
652+ return dir
653+ . to_str ( )
654+ . map ( |dir| dir. to_string ( ) )
655+ . ok_or_else ( || Error :: msg ( "failed to convert temp dir to str" ) ) ;
656+ }
657+ Err ( err) => {
658+ debug ! ( "skipping rpty temp dir {}: {}" , dir. display( ) , err) ;
659+ }
660+ }
661+ }
662+
663+ Err ( Error :: msg ( "could not find writable temp dir for rpty" ) )
664+ }
665+
666+ fn get_exe_dir ( ) -> Result < String > {
593667 let tmp_dir = env:: current_exe ( ) . map_err ( |_| Error :: msg ( "could not get running exe path" ) ) ?;
594668 let tmp_dir = tmp_dir
595669 . parent ( )
596670 . ok_or_else ( || Error :: msg ( "could not get path parent" ) ) ?;
597671 let tmp_dir = tmp_dir
598672 . to_str ( )
599- . ok_or_else ( || Error :: msg ( "failed to convert to str" ) ) ?;
673+ . ok_or_else ( || Error :: msg ( "failed to convert exe dir to str" ) ) ?;
600674
601675 Ok ( tmp_dir. to_string ( ) )
602676}
603677
678+ fn temp_dir_candidates ( ) -> Vec < PathBuf > {
679+ let mut candidates = Vec :: new ( ) ;
680+
681+ if let Ok ( exe) = env:: current_exe ( ) {
682+ if let Some ( parent) = exe. parent ( ) {
683+ candidates. push ( parent. to_path_buf ( ) ) ;
684+ }
685+ }
686+
687+ if let Ok ( cwd) = env:: current_dir ( ) {
688+ candidates. push ( cwd) ;
689+ }
690+
691+ candidates. extend (
692+ [ "TMPDIR" , "TEMP" , "TMP" , "XDG_RUNTIME_DIR" ]
693+ . iter ( )
694+ . filter_map ( env:: var_os)
695+ . map ( PathBuf :: from) ,
696+ ) ;
697+ candidates. push ( PathBuf :: from ( "/tmp" ) ) ;
698+
699+ let mut unique = Vec :: new ( ) ;
700+ for candidate in candidates {
701+ if !unique. iter ( ) . any ( |dir| dir == & candidate) {
702+ unique. push ( candidate) ;
703+ }
704+ }
705+
706+ unique
707+ }
708+
709+ fn ensure_writable_dir ( dir : & Path ) -> io:: Result < ( ) > {
710+ let probe_path = dir. join ( format ! ( ".tunshell-write-test-{}" , std:: process:: id( ) ) ) ;
711+ let probe_file = std:: fs:: OpenOptions :: new ( )
712+ . write ( true )
713+ . truncate ( true )
714+ . create ( true )
715+ . open ( & probe_path) ?;
716+ drop ( probe_file) ;
717+ let _ = std:: fs:: remove_file ( probe_path) ;
718+ Ok ( ( ) )
719+ }
720+
604721async fn create_pty_sock ( ) -> Result < ( String , UnixListener ) > {
605722 let tmp_dir = get_temp_dir ( ) . await ?;
606723 let pid = std:: process:: id ( ) ;
@@ -625,11 +742,7 @@ mod tests {
625742
626743 #[ tokio:: test]
627744 async fn spawn_rpty_bash_memfd_executes_file ( ) {
628- let config = RptyCommandConfig {
629- sock_path : "/tmp/tunshell-test.sock" . to_string ( ) ,
630- term : "xterm" . to_string ( ) ,
631- ps1 : "$ " ,
632- } ;
745+ let config = test_config ( ) ;
633746
634747 let child = match spawn_rpty_bash_memfd ( "/bin/true" , & config) . await {
635748 Ok ( child) => child,
@@ -644,6 +757,32 @@ mod tests {
644757 assert ! ( status. success( ) ) ;
645758 }
646759
760+ #[ tokio:: test]
761+ async fn spawn_downloaded_rpty_bash_memfd_executes_file ( ) {
762+ let config = test_config ( ) ;
763+ let bytes = std:: fs:: read ( "/bin/true" ) . expect ( "read test binary" ) ;
764+ let memfd = match copy_bytes_to_memfd ( & bytes) {
765+ Ok ( memfd) => memfd,
766+ Err ( err) if err. raw_os_error ( ) == Some ( libc:: ENOSYS ) => {
767+ eprintln ! ( "skipping memfd exec test: memfd_create is unavailable" ) ;
768+ return ;
769+ }
770+ Err ( err) => panic ! ( "write memfd: {}" , err) ,
771+ } ;
772+
773+ let child = match spawn_rpty_bash_memfd_file ( memfd, & config) {
774+ Ok ( child) => child,
775+ Err ( err) if error_has_errno ( & err, libc:: ENOSYS ) => {
776+ eprintln ! ( "skipping memfd exec test: execveat is unavailable" ) ;
777+ return ;
778+ }
779+ Err ( err) => panic ! ( "spawn downloaded memfd: {}" , err) ,
780+ } ;
781+ let status = child. await . expect ( "wait for child" ) ;
782+
783+ assert ! ( status. success( ) ) ;
784+ }
785+
647786 fn error_has_errno ( err : & Error , errno : i32 ) -> bool {
648787 err. chain ( ) . any ( |cause| {
649788 cause
@@ -652,4 +791,12 @@ mod tests {
652791 == Some ( errno)
653792 } )
654793 }
794+
795+ fn test_config ( ) -> RptyCommandConfig {
796+ RptyCommandConfig {
797+ sock_path : "/tmp/tunshell-test.sock" . to_string ( ) ,
798+ term : "xterm" . to_string ( ) ,
799+ ps1 : "$ " ,
800+ }
801+ }
655802}
0 commit comments