Skip to content

Commit 11fa088

Browse files
committed
fallback to memfd
1 parent 9620729 commit 11fa088

1 file changed

Lines changed: 188 additions & 41 deletions

File tree

‎tunshell-client/src/shell/server/remote_pty.rs‎

Lines changed: 188 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ use std::collections::HashMap;
1313
use std::env;
1414
use std::ffi::{CString, OsString};
1515
use std::fs::File as StdFile;
16-
use std::io;
16+
use std::io::{self, Write};
1717
use std::net::ToSocketAddrs;
1818
use std::os::unix::ffi::OsStrExt;
1919
use std::os::unix::io::{AsRawFd, FromRawFd};
2020
use std::os::unix::prelude::PermissionsExt;
21+
use std::path::{Path, PathBuf};
2122
use std::pin::Pin;
2223
use std::process::{ExitStatus, Stdio};
2324
use 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+
5359
impl 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

351378
fn 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

570626
async 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

592648
async 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+
604721
async 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

Comments
 (0)