diff --git a/internal/agent/ship.go b/internal/agent/ship.go index bcc2ef4..9060104 100644 --- a/internal/agent/ship.go +++ b/internal/agent/ship.go @@ -562,6 +562,14 @@ func (u *unpacker) own(p string) { // shq quotes s for a POSIX shell. func shq(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } +// posix wraps a command for the host's sh: the user's login shell gets one +// word to hand on, so fish or csh there does not matter. +func posix(cmd string) string { + // csh reads "!" as history even inside single quotes; '\!' is a plain + // "!" for it and for sh alike. + return "sh -c " + strings.ReplaceAll(shq(cmd), "!", `'\!'`) +} + // shipSSH writes the tar stream into tar on the SSH host, in Dir. func (a *Agent) shipSSH(ctx context.Context, args proto.FSShipArgs, src string, m *shipMeter) error { client, _, err := a.sshConnect(ctx, *args.SSH, func(_, note string) { m.note.Store(note) }) @@ -615,7 +623,7 @@ func (a *Agent) shipSSH(ctx context.Context, args proto.FSShipArgs, src string, return err } cmd := "mkdir -p -- " + shq(dir) + " && cd -- " + shq(dir) + " && tar -xf -" + flags - if err := sess.Start(cmd); err != nil { + if err := sess.Start(posix(cmd)); err != nil { return err } m.note.Store("") @@ -646,7 +654,7 @@ func sshRun(c *ssh.Client, cmd string) (string, error) { return "", err } defer s.Close() - out, err := s.Output(cmd) + out, err := s.Output(posix(cmd)) var ee *ssh.ExitError if errors.As(err, &ee) { err = nil // the checks may exit non-zero; what they print is the answer diff --git a/internal/agent/sshfs.go b/internal/agent/sshfs.go index 3434575..2ee4f49 100644 --- a/internal/agent/sshfs.go +++ b/internal/agent/sshfs.go @@ -246,7 +246,7 @@ func (a *Agent) sshfsRun(c *sshfsConn, cmd string, stdin io.Reader) ([]byte, err if stdin != nil { s.Stdin = stdin } - if err := s.Run(cmd); err != nil { + if err := s.Run(posix(cmd)); err != nil { msg := strings.TrimSpace(errb.String()) if msg == "" { msg = err.Error() @@ -535,7 +535,7 @@ func (a *Agent) doSSHFSSend(ctx context.Context, env proto.TaskEnvelope) proto.T } defer s.Close() out, _ := s.StdoutPipe() - if err := s.Start("cat -- " + shq(args.Path)); err != nil { + if err := s.Start(posix("cat -- " + shq(args.Path))); err != nil { return proto.TaskResult{Error: err.Error()} } // Read in order from cat, posted to the server a few at a time. @@ -612,7 +612,7 @@ func (a *Agent) doSSHFSPut(ctx context.Context, env proto.TaskEnvelope) proto.Ta in, _ := s.StdinPipe() var errb bytes.Buffer s.Stderr = &limitBuf{b: &errb, max: 4096} - if err := s.Start(writeCommand(args.Path)); err != nil { + if err := s.Start(posix(writeCommand(args.Path))); err != nil { return proto.TaskResult{Error: err.Error()} } // Pieces come in a few ahead; stdin takes them in order. ack: how many @@ -711,7 +711,7 @@ func (a *Agent) packRemote(ctx context.Context, w io.Writer, c *sshfsConn, srcs, rename[b] = names[i] cmd += " " + shq(b) } - if err := s.Start(cmd); err != nil { + if err := s.Start(posix(cmd)); err != nil { return err } if err := renameTar(w, &countReader{r: out, n: &m.done}, rename); err != nil { diff --git a/internal/agent/sshfs_test.go b/internal/agent/sshfs_test.go index b3797aa..7dc4e67 100644 --- a/internal/agent/sshfs_test.go +++ b/internal/agent/sshfs_test.go @@ -225,3 +225,18 @@ func TestListFolderLink(t *testing.T) { } } } + +// Commands go to the host as one argument of sh -c, so the user's own login +// shell (fish, csh) cannot read them its way. +func TestPosixCommand(t *testing.T) { + if got := posix(`if [ -d '/x' ]; then echo ok; fi`); got != `sh -c 'if [ -d '\''/x'\'' ]; then echo ok; fi'` { + t.Fatalf("%s", got) + } +} + +// csh expands "!" even inside single quotes: it goes out as '\!'. +func TestPosixCommandBang(t *testing.T) { + if got := posix(`ls .[!.]*`); got != `sh -c 'ls .['\!'.]*'` { + t.Fatalf("%s", got) + } +}