Skip to content

Commit 242ace2

Browse files
committed
fix(cli): isolate ssh from host linker environment
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 90cb713 commit 242ace2

1 file changed

Lines changed: 123 additions & 0 deletions

File tree

  • crates/openshell-cli/src

crates/openshell-cli/src/ssh.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ use tokio::process::Command as TokioCommand;
2929
use tokio_stream::wrappers::ReceiverStream;
3030

3131
const FOREGROUND_FORWARD_STARTUP_GRACE_PERIOD: Duration = Duration::from_secs(2);
32+
const HOST_TOOL_LINKER_ENV: &[&str] = &[
33+
"DYLD_FALLBACK_LIBRARY_PATH",
34+
"DYLD_INSERT_LIBRARIES",
35+
"DYLD_LIBRARY_PATH",
36+
"LD_AUDIT",
37+
"LD_LIBRARY_PATH",
38+
"LD_PRELOAD",
39+
"LIBRARY_PATH",
40+
"NIX_LD_LIBRARY_PATH",
41+
];
3242

3343
#[derive(Clone, Copy, Debug)]
3444
pub enum Editor {
@@ -121,6 +131,7 @@ async fn ssh_session_config(
121131
&session.token,
122132
gateway_name,
123133
);
134+
let proxy_command = proxy_command_with_preserved_environment(proxy_command);
124135

125136
Ok(SshSessionConfig {
126137
proxy_command,
@@ -137,6 +148,7 @@ fn ssh_base_command(proxy_command: &str) -> Command {
137148
std::env::var("OPENSHELL_SSH_LOG_LEVEL").unwrap_or_else(|_| "ERROR".to_string());
138149

139150
let mut command = Command::new("ssh");
151+
sanitize_host_tool_environment(&mut command);
140152
command
141153
.arg("-o")
142154
.arg(format!("ProxyCommand={proxy_command}"))
@@ -159,6 +171,30 @@ fn ssh_base_command(proxy_command: &str) -> Command {
159171
command
160172
}
161173

174+
fn sanitize_host_tool_environment(command: &mut Command) {
175+
for key in HOST_TOOL_LINKER_ENV {
176+
command.env_remove(key);
177+
}
178+
}
179+
180+
fn proxy_command_with_preserved_environment(proxy_command: String) -> String {
181+
let assignments = HOST_TOOL_LINKER_ENV
182+
.iter()
183+
.filter_map(|key| {
184+
std::env::var_os(key).map(|value| {
185+
let value = value.to_string_lossy();
186+
format!("{key}={}", shell_escape(&value))
187+
})
188+
})
189+
.collect::<Vec<_>>();
190+
191+
if assignments.is_empty() {
192+
proxy_command
193+
} else {
194+
format!("env {} {proxy_command}", assignments.join(" "))
195+
}
196+
}
197+
162198
#[cfg(unix)]
163199
const TRANSIENT_TTY_SIGNALS: &[Signal] = &[Signal::SIGINT, Signal::SIGQUIT, Signal::SIGTERM];
164200

@@ -1508,6 +1544,93 @@ mod tests {
15081544
use super::*;
15091545
use crate::TEST_ENV_LOCK;
15101546

1547+
#[test]
1548+
fn ssh_base_command_removes_host_linker_environment() {
1549+
let command = ssh_base_command("openshell ssh-proxy");
1550+
let removed_keys = command
1551+
.get_envs()
1552+
.filter(|(_, value)| value.is_none())
1553+
.map(|(key, _)| key.to_string_lossy().into_owned())
1554+
.collect::<Vec<_>>();
1555+
1556+
for key in HOST_TOOL_LINKER_ENV {
1557+
assert!(
1558+
removed_keys.iter().any(|removed| removed == key),
1559+
"expected ssh command to remove {key}"
1560+
);
1561+
}
1562+
}
1563+
1564+
#[test]
1565+
#[allow(unsafe_code)] // Test-only: env vars require unsafe in Rust 2024.
1566+
fn proxy_command_preserves_linker_environment_for_proxy_child() {
1567+
let _guard = TEST_ENV_LOCK
1568+
.lock()
1569+
.unwrap_or_else(std::sync::PoisonError::into_inner);
1570+
let old_env = HOST_TOOL_LINKER_ENV
1571+
.iter()
1572+
.map(|key| (*key, std::env::var_os(key)))
1573+
.collect::<Vec<_>>();
1574+
1575+
unsafe {
1576+
for key in HOST_TOOL_LINKER_ENV {
1577+
std::env::remove_var(key);
1578+
}
1579+
std::env::set_var("LD_LIBRARY_PATH", "/nix/store/z3 lib:/opt/lib");
1580+
}
1581+
1582+
let proxy_command =
1583+
proxy_command_with_preserved_environment("openshell ssh-proxy".to_string());
1584+
let has_assignment = proxy_command.contains("LD_LIBRARY_PATH='/nix/store/z3 lib:/opt/lib'");
1585+
let has_env_prefix = proxy_command.starts_with("env ");
1586+
let has_command = proxy_command.ends_with(" openshell ssh-proxy");
1587+
1588+
unsafe {
1589+
for (key, value) in old_env {
1590+
match value {
1591+
Some(value) => std::env::set_var(key, value),
1592+
None => std::env::remove_var(key),
1593+
}
1594+
}
1595+
}
1596+
1597+
assert!(has_assignment, "unexpected proxy command: {proxy_command}");
1598+
assert!(has_env_prefix, "unexpected proxy command: {proxy_command}");
1599+
assert!(has_command, "unexpected proxy command: {proxy_command}");
1600+
}
1601+
1602+
#[test]
1603+
#[allow(unsafe_code)] // Test-only: env vars require unsafe in Rust 2024.
1604+
fn proxy_command_is_unchanged_without_linker_environment() {
1605+
let _guard = TEST_ENV_LOCK
1606+
.lock()
1607+
.unwrap_or_else(std::sync::PoisonError::into_inner);
1608+
let old_env = HOST_TOOL_LINKER_ENV
1609+
.iter()
1610+
.map(|key| (*key, std::env::var_os(key)))
1611+
.collect::<Vec<_>>();
1612+
1613+
unsafe {
1614+
for key in HOST_TOOL_LINKER_ENV {
1615+
std::env::remove_var(key);
1616+
}
1617+
}
1618+
1619+
let proxy_command =
1620+
proxy_command_with_preserved_environment("openshell ssh-proxy".to_string());
1621+
1622+
unsafe {
1623+
for (key, value) in old_env {
1624+
match value {
1625+
Some(value) => std::env::set_var(key, value),
1626+
None => std::env::remove_var(key),
1627+
}
1628+
}
1629+
}
1630+
1631+
assert_eq!(proxy_command, "openshell ssh-proxy");
1632+
}
1633+
15111634
#[test]
15121635
fn upsert_host_block_appends_when_missing() {
15131636
let input = "Host existing\n HostName example.com\n";

0 commit comments

Comments
 (0)