fix(agent): unify working directory resolution (#26102)

agentssh's CommandEnv, sftpHandler, and agentproc each resolved the
session working directory on their own and had drifted: sftpHandler used
the configured directory without checking it exists and bypassed the
injected EnvInfoer, while the others stat-checked and fell back to home.
Home and shell lookups could also skip the EnvInfoer seam through the
exported usershell.HomeDir and Get.

Resolve through a single usershell.ResolveWorkingDirectory and confine
host home and shell lookups to usershell, so SSH sessions, the process
API, and tests can no longer diverge and the injected environment is
always honored. This also fixes SFTP landing in a configured directory
that no longer exists.

Refs coder/coder#26099
This commit is contained in:
Mathias Fredriksson
2026-06-08 14:24:32 +03:00
committed by GitHub
parent 1e5dd83a95
commit 3955df796e
11 changed files with 231 additions and 116 deletions
+23 -47
View File
@@ -9,7 +9,6 @@ import (
"net"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"slices"
@@ -185,13 +184,9 @@ func NewServer(ctx context.Context, logger slog.Logger, prometheusRegistry *prom
config.AnnouncementBanners = func() *[]codersdk.BannerConfig { return &[]codersdk.BannerConfig{} }
}
if config.WorkingDirectory == nil {
config.WorkingDirectory = func() string {
home, err := userHomeDir()
if err != nil {
return ""
}
return home
}
// Empty means unset, so resolveWorkingDirectory falls back to the
// EnvInfo home directory.
config.WorkingDirectory = func() string { return "" }
}
if config.EnvInfo == nil {
config.EnvInfo = &usershell.SystemEnvInfo{}
@@ -749,7 +744,7 @@ func (s *Server) startPTYSession(logger slog.Logger, session ptySession, magicTy
}
}
if !isQuietLogin(s.fs, session.RawCommand()) {
if !isQuietLogin(s.fs, s.config.EnvInfo, session.RawCommand()) {
err := showMOTD(s.fs, session, s.config.MOTDFile())
if err != nil {
logger.Error(ctx, "agent failed to show MOTD", slog.Error(err))
@@ -878,13 +873,14 @@ func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) error {
// Change current working directory to the configured
// directory (or home directory if not set) so that SFTP
// connections land there.
dir := s.config.WorkingDirectory()
if dir == "" {
var err error
dir, err = userHomeDir()
if err != nil {
logger.Warn(ctx, "get sftp working directory failed, unable to get home dir", slog.Error(err))
}
//
// The host EnvInfo is used here, not a container's. This is
// correct only while SFTP is blocked for container sessions
// (see the closeCause guard above). If container SFTP is added,
// the container EnvInfo must be resolved and passed here.
dir, err := s.resolveWorkingDirectory(s.config.EnvInfo)
if err != nil {
logger.Warn(ctx, "resolve sftp working directory failed", slog.Error(err))
}
if dir != "" {
opts = append(opts, sftp.WithServerWorkingDirectory(dir))
@@ -916,6 +912,12 @@ func (s *Server) sftpHandler(logger slog.Logger, session ssh.Session) error {
return xerrors.Errorf("sftp server closed with error: %w", err)
}
// resolveWorkingDirectory returns the working directory for a session, binding
// the server filesystem and configured directory to the shared resolver.
func (s *Server) resolveWorkingDirectory(ei usershell.EnvInfoer) (string, error) {
return usershell.ResolveWorkingDirectory(s.fs, ei, s.config.WorkingDirectory())
}
func (s *Server) CommandEnv(ei usershell.EnvInfoer, addEnv []string) (shell, dir string, env []string, err error) {
if ei == nil {
ei = &usershell.SystemEnvInfo{}
@@ -932,18 +934,9 @@ func (s *Server) CommandEnv(ei usershell.EnvInfoer, addEnv []string) (shell, dir
return "", "", nil, xerrors.Errorf("get user shell: %w", err)
}
dir = s.config.WorkingDirectory()
// If the metadata directory doesn't exist, we run the command
// in the users home directory.
_, err = os.Stat(dir)
if dir == "" || err != nil {
// Default to user home if a directory is not set.
homedir, err := ei.HomeDir()
if err != nil {
return "", "", nil, xerrors.Errorf("get home dir: %w", err)
}
dir = homedir
dir, err = s.resolveWorkingDirectory(ei)
if err != nil {
return "", "", nil, xerrors.Errorf("resolve working dir: %w", err)
}
env = append(ei.Environ(), addEnv...)
// Set login variables (see `man login`).
@@ -1288,7 +1281,7 @@ func isLoginShell(rawCommand string) bool {
// isQuietLogin checks if the SSH server should perform a quiet login or not.
//
// https://github.com/openssh/openssh-portable/blob/25bd659cc72268f2858c5415740c442ee950049f/session.c#L816
func isQuietLogin(fs afero.Fs, rawCommand string) bool {
func isQuietLogin(fs afero.Fs, ei usershell.EnvInfoer, rawCommand string) bool {
// We are always quiet unless this is a login shell.
if !isLoginShell(rawCommand) {
return true
@@ -1296,7 +1289,7 @@ func isQuietLogin(fs afero.Fs, rawCommand string) bool {
// Best effort, if we can't get the home directory,
// we can't lookup .hushlogin.
homedir, err := userHomeDir()
homedir, err := ei.HomeDir()
if err != nil {
return false
}
@@ -1355,23 +1348,6 @@ func writeWithCarriageReturn(src io.Reader, dest io.Writer) error {
return nil
}
// userHomeDir returns the home directory of the current user, giving
// priority to the $HOME environment variable.
func userHomeDir() (string, error) {
// First we check the environment.
homedir, err := os.UserHomeDir()
if err == nil {
return homedir, nil
}
// As a fallback, we try the user information.
u, err := user.Current()
if err != nil {
return "", xerrors.Errorf("current user: %w", err)
}
return u.HomeDir, nil
}
// UpdateHostSigner updates the host signer with a new key generated from the provided seed.
// If an existing host key exists with the same algorithm, it is overwritten
func (s *Server) UpdateHostSigner(seed int64) error {