diff --git a/app/assets/bundled/bootstrap/bash_body.sh b/app/assets/bundled/bootstrap/bash_body.sh index f1b3a973c..d7fff84be 100644 --- a/app/assets/bundled/bootstrap/bash_body.sh +++ b/app/assets/bundled/bootstrap/bash_body.sh @@ -985,6 +985,20 @@ if [ -z "$WARP_BOOTSTRAPPED" ]; then # determine what shell is the login shell on the remote machine. We perform a preliminary check to see if # the remote shell is the Bourne shell to avoid asking it to parse later lines that use syntax it doesn't # support. + # + # MotD emulation note (tracks GH-1160). The heredoc below emulates the + # MotD print that SSHD normally does at login. SSHD skips MotD when + # invoked with a command, and the bash/zsh rcfile bootstrap further + # down does not reintroduce it, so before this fix bash/zsh users + # silently lost the MotD over Warp SSH. Probe order: /etc/motd, + # then /etc/motd.d (Fedora/RHEL fragments), then /run/motd.dynamic + # (Ubuntu/Debian pre-rendered MotD), then the legacy paths. + # The motd_emitted flag tracks fall-through so an empty /etc/motd.d + # does not block later candidates. Variables set by the remote + # shell (motd_emitted, the for-loop iterator) are referenced as + # backslash-dollar (e.g. \$motd_fragment) so the local shell does + # not expand the dollar-sign before the heredoc reaches the remote + # -- same escape pattern as the zshenv decode loop below. command ssh -o ControlMaster=yes -o ControlPath=$SSH_SOCKET_DIR/$WARP_SESSION_ID \ -t "${@:1}" \ " @@ -1000,26 +1014,37 @@ test -n '$WARP_CLI_AGENT_PROTOCOL_VERSION' && export WARP_CLI_AGENT_PROTOCOL_VER hook="'$(printf "{\"hook\": \"SSH\", \"value\": {\"socket_path\": \"'$SSH_SOCKET_DIR/$WARP_SESSION_ID'\", \"remote_shell\": \"%s\"}}" "${SHELL##*/}" | command -p od -An -v -tx1 | command -p tr -d " \n")'" printf '$OSC_START$DCS_JSON_MARKER$OSC_PARAM_SEPARATOR%s$OSC_END' "'$hook'" -if test "'"${SHELL##*/}" != "bash" -a "${SHELL##*/}" != "zsh"'"; then - # Emulate the SSHD logic to print the MotD. Because the Warp SSH wrapper passes - # a command to run, SSHD does a quiet login, updating utmp and other login - # state, but not printing the MotD. For bash and zsh, this is instead handled - # by our bootstrap script. - if test ! -e "'$HOME/.hushlogin'"; then - # This uses an if-else chain instead of a for-loop to avoid expansion issues on older shells. - if test -r /etc/motd; then - command -p cat /etc/motd +# GH-1160: emulate the MotD print SSHD skips for command-passing invocations. +if test ! -e "'$HOME/.hushlogin'"; then + motd_emitted=0 + if test -f /etc/motd && test -r /etc/motd; then + command -p cat /etc/motd + motd_emitted=1 + fi + if test "\$motd_emitted" = 0 && test -d /etc/motd.d; then + for motd_fragment in /etc/motd.d/*; do + if test -f "\$motd_fragment" && test -r "\$motd_fragment"; then + command -p cat "\$motd_fragment" + motd_emitted=1 + fi + done + fi + if test "\$motd_emitted" = 0; then + if test -r /run/motd.dynamic; then + command -p cat /run/motd.dynamic elif test -r /run/motd; then command -p cat /run/motd - elif test -r /run/motd.dynamic; then - command -p cat /run/motd.dynamic elif test -r /usr/lib/motd; then command -p cat /usr/lib/motd elif test -r /usr/lib/motd.dynamic; then command -p cat /usr/lib/motd.dynamic fi fi - # Likewise, emulate a login shell by sourcing /etc/profile +fi + +if test "'"${SHELL##*/}" != "bash" -a "${SHELL##*/}" != "zsh"'"; then + # Source /etc/profile for non-bash/non-zsh shells; bash and zsh load + # their own profile/rcfile chain via the case statement below. if test -r /etc/profile; then . /etc/profile fi diff --git a/app/assets/bundled/bootstrap/fish.sh b/app/assets/bundled/bootstrap/fish.sh index 235df3b77..83572d36b 100644 --- a/app/assets/bundled/bootstrap/fish.sh +++ b/app/assets/bundled/bootstrap/fish.sh @@ -609,6 +609,20 @@ if test "$WARP_IS_LOCAL_SHELL_SESSION" = "1" # determine what shell is the login shell on the remote machine. We perform a preliminary check to see if # the remote shell is the Bourne shell to avoid asking it to parse later lines that use syntax it doesn't # support. + # + # MotD emulation note (tracks GH-1160). The heredoc below emulates the + # MotD print that SSHD normally does at login. SSHD skips MotD when + # invoked with a command, and the bash/zsh rcfile bootstrap further + # down does not reintroduce it, so before this fix bash/zsh users + # silently lost the MotD over Warp SSH. Probe order: /etc/motd, + # then /etc/motd.d (Fedora/RHEL fragments), then /run/motd.dynamic + # (Ubuntu/Debian pre-rendered MotD), then the legacy paths. + # The motd_emitted flag tracks fall-through so an empty /etc/motd.d + # does not block later candidates. Variables set by the remote + # shell (motd_emitted, the for-loop iterator) are referenced as + # backslash-dollar (e.g. \$motd_fragment) so the local shell does + # not expand the dollar-sign before the heredoc reaches the remote + # -- same escape pattern as the zshenv decode loop below. command ssh -o ControlMaster=yes -o ControlPath=$SSH_SOCKET_DIR/$WARP_SESSION_ID \ -t $argv \ " @@ -619,26 +633,37 @@ test -n '$WARP_CLI_AGENT_PROTOCOL_VERSION' && export WARP_CLI_AGENT_PROTOCOL_VER hook="'$(printf "{\"hook\": \"SSH\", \"value\": {\"socket_path\": \"'$SSH_SOCKET_DIR/$WARP_SESSION_ID'\", \"remote_shell\": \"%s\"}}" "${SHELL##*/}" | command od -An -v -tx1 | command tr -d " \n")'" printf '$DCS_START$DCS_JSON_MARKER%s$DCS_END' "'$hook'" -if test "'"${SHELL##*/}" != "bash" -a "${SHELL##*/}" != "zsh"'"; then - # Emulate the SSHD logic to print the MotD. Because the Warp SSH wrapper passes - # a command to run, SSHD does a quiet login, updating utmp and other login - # state, but not printing the MotD. For bash and zsh, this is instead handled - # by our bootstrap script. - if test ! -e "'$HOME/.hushlogin'"; then - # This uses an if-else chain instead of a for-loop to avoid expansion issues on older shells. - if test -r /etc/motd; then - cat /etc/motd +# GH-1160: emulate the MotD print SSHD skips for command-passing invocations. +if test ! -e "'$HOME/.hushlogin'"; then + motd_emitted=0 + if test -f /etc/motd && test -r /etc/motd; then + cat /etc/motd + motd_emitted=1 + fi + if test "\$motd_emitted" = 0 && test -d /etc/motd.d; then + for motd_fragment in /etc/motd.d/*; do + if test -f "\$motd_fragment" && test -r "\$motd_fragment"; then + cat "\$motd_fragment" + motd_emitted=1 + fi + done + fi + if test "\$motd_emitted" = 0; then + if test -r /run/motd.dynamic; then + cat /run/motd.dynamic elif test -r /run/motd; then cat /run/motd - elif test -r /run/motd.dynamic; then - cat /run/motd.dynamic elif test -r /usr/lib/motd; then cat /usr/lib/motd elif test -r /usr/lib/motd.dynamic; then cat /usr/lib/motd.dynamic fi fi - # Likewise, emulate a login shell by sourcing /etc/profile +fi + +if test "'"${SHELL##*/}" != "bash" -a "${SHELL##*/}" != "zsh"'"; then + # Source /etc/profile for non-bash/non-zsh shells; bash and zsh load + # their own profile/rcfile chain via the case statement below. if test -r /etc/profile; then . /etc/profile fi diff --git a/app/assets/bundled/bootstrap/zsh_body.sh b/app/assets/bundled/bootstrap/zsh_body.sh index 45bd80580..257d7c6c4 100644 --- a/app/assets/bundled/bootstrap/zsh_body.sh +++ b/app/assets/bundled/bootstrap/zsh_body.sh @@ -876,6 +876,20 @@ if [[ -z $WARP_BOOTSTRAPPED ]]; then # determine what shell is the login shell on the remote machine. We perform a preliminary check to see if # the remote shell is the Bourne shell to avoid asking it to parse later lines that use syntax it doesn't # support. + # + # MotD emulation note (tracks GH-1160). The heredoc below emulates the + # MotD print that SSHD normally does at login. SSHD skips MotD when + # invoked with a command, and the bash/zsh rcfile bootstrap further + # down does not reintroduce it, so before this fix bash/zsh users + # silently lost the MotD over Warp SSH. Probe order: /etc/motd, + # then /etc/motd.d (Fedora/RHEL fragments), then /run/motd.dynamic + # (Ubuntu/Debian pre-rendered MotD), then the legacy paths. + # The motd_emitted flag tracks fall-through so an empty /etc/motd.d + # does not block later candidates. Variables set by the remote + # shell (motd_emitted, the for-loop iterator) are referenced as + # backslash-dollar (e.g. \$motd_fragment) so the local shell does + # not expand the dollar-sign before the heredoc reaches the remote + # -- same escape pattern as the zshenv decode loop below. command ssh -o ControlMaster=yes -o ControlPath=$SSH_SOCKET_DIR/$WARP_SESSION_ID \ -t "${@:1}" \ " @@ -890,26 +904,37 @@ test -n '$WARP_CLI_AGENT_PROTOCOL_VERSION' && export WARP_CLI_AGENT_PROTOCOL_VER hook="'$(printf "{\"hook\": \"SSH\", \"value\": {\"socket_path\": \"'$SSH_SOCKET_DIR/$WARP_SESSION_ID'\", \"remote_shell\": \"%s\"}}" "${SHELL##*/}" | command -p od -An -v -tx1 | command -p tr -d " \n")'" printf '$OSC_START$DCS_JSON_MARKER$OSC_PARAM_SEPARATOR%s$OSC_END' "'$hook'" -if test "'"${SHELL##*/}" != "bash" -a "${SHELL##*/}" != "zsh"'"; then - # Emulate the SSHD logic to print the MotD. Because the Warp SSH wrapper passes - # a command to run, SSHD does a quiet login, updating utmp and other login - # state, but not printing the MotD. For bash and zsh, this is instead handled - # by our bootstrap script. - if test ! -e "'$HOME/.hushlogin'"; then - # This uses an if-else chain instead of a for-loop to avoid expansion issues on older shells. - if test -r /etc/motd; then - command -p cat /etc/motd +# GH-1160: emulate the MotD print SSHD skips for command-passing invocations. +if test ! -e "'$HOME/.hushlogin'"; then + motd_emitted=0 + if test -f /etc/motd && test -r /etc/motd; then + command -p cat /etc/motd + motd_emitted=1 + fi + if test "\$motd_emitted" = 0 && test -d /etc/motd.d; then + for motd_fragment in /etc/motd.d/*; do + if test -f "\$motd_fragment" && test -r "\$motd_fragment"; then + command -p cat "\$motd_fragment" + motd_emitted=1 + fi + done + fi + if test "\$motd_emitted" = 0; then + if test -r /run/motd.dynamic; then + command -p cat /run/motd.dynamic elif test -r /run/motd; then command -p cat /run/motd - elif test -r /run/motd.dynamic; then - command -p cat /run/motd.dynamic elif test -r /usr/lib/motd; then command -p cat /usr/lib/motd elif test -r /usr/lib/motd.dynamic; then command -p cat /usr/lib/motd.dynamic fi fi - # Likewise, emulate a login shell by sourcing /etc/profile +fi + +if test "'"${SHELL##*/}" != "bash" -a "${SHELL##*/}" != "zsh"'"; then + # Source /etc/profile for non-bash/non-zsh shells; bash and zsh load + # their own profile/rcfile chain via the case statement below. if test -r /etc/profile; then . /etc/profile fi diff --git a/app/src/terminal/bootstrap_test.rs b/app/src/terminal/bootstrap_test.rs index 18f13a838..01648a32a 100644 --- a/app/src/terminal/bootstrap_test.rs +++ b/app/src/terminal/bootstrap_test.rs @@ -62,3 +62,64 @@ fn test_trims_powershell_specifics() { fn decode_script(bytes: &[u8]) -> &str { std::str::from_utf8(bytes).expect("should not fail to decode") } + +/// Regression test for GH-1160. +/// +/// Until this fix, the MotD-emulation block in each shell-bootstrap body was +/// nested inside an `if test "${SHELL##*/}" != "bash" -a "${SHELL##*/}" != +/// "zsh"` guard, with a comment claiming MotD was "instead handled by our +/// bootstrap script" for bash and zsh. It wasn't — sshd skips MotD for +/// command-passing invocations and Warp's bash/zsh rcfile bootstrap doesn't +/// reintroduce it, so bash and zsh users silently lost the MotD over Warp SSH. +/// +/// This test asserts the structural invariant after the fix: in each of +/// `bash_body.sh`, `zsh_body.sh`, and `fish.sh` the MotD-print branch +/// (identified by `cat /etc/motd` / `cat /run/motd.dynamic`) appears **before** +/// the `!= "bash" -a` shell-type guard, so it runs unconditionally. +#[test] +fn test_motd_emulation_is_not_gated_on_shell_type() { + const BASH_BODY: &str = include_str!("../../assets/bundled/bootstrap/bash_body.sh"); + const ZSH_BODY: &str = include_str!("../../assets/bundled/bootstrap/zsh_body.sh"); + const FISH_BODY: &str = include_str!("../../assets/bundled/bootstrap/fish.sh"); + + for (shell, body) in [ + ("bash_body.sh", BASH_BODY), + ("zsh_body.sh", ZSH_BODY), + ("fish.sh", FISH_BODY), + ] { + // Marker for the actual MotD-print probe. We deliberately match the + // *executable* `test -f /etc/motd && test -r /etc/motd` line and not + // bare `/etc/motd`, because `/etc/motd` also appears in the + // surrounding explanatory comments — a pre-fix file that kept the + // comments but removed the executable probe would still pass a + // `body.find("/etc/motd")` check, defeating the test. + let motd_marker = "test -f /etc/motd && test -r /etc/motd"; + // Marker for the non-bash/non-zsh guard. The `!= "bash" -a` substring + // is stable across the three heredocs. + let guard_marker = "!= \"bash\" -a"; + + let motd_idx = body.find(motd_marker).unwrap_or_else(|| { + panic!( + "{shell}: executable MotD probe ({motd_marker:?}) not found. \ + If the probe structure changed, update this regression test \ + (and verify GH-1160 doesn't regress)." + ) + }); + let guard_idx = body.find(guard_marker).unwrap_or_else(|| { + panic!( + "{shell}: non-bash/non-zsh guard ({guard_marker:?}) not found. \ + If the heredoc structure changed, update this regression test \ + (and verify GH-1160 doesn't regress)." + ) + }); + + assert!( + motd_idx < guard_idx, + "GH-1160: in `{shell}` the executable MotD probe (offset {motd_idx}) \ + must appear BEFORE the non-bash/non-zsh guard (offset {guard_idx}) \ + so MotD prints for bash and zsh too. Putting the MotD block back \ + inside the guard silently regresses GH-1160 (bash/zsh users will \ + no longer see /etc/motd over Warp SSH)." + ); + } +}