@@ -29,6 +29,16 @@ use tokio::process::Command as TokioCommand;
2929use tokio_stream:: wrappers:: ReceiverStream ;
3030
3131const 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 ) ]
3444pub 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) ]
163199const 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