From 8f5ebd2b5d31e88b63620e70c9582dbab2af57ae Mon Sep 17 00:00:00 2001 From: Aethernode Date: Sat, 20 Jun 2026 04:12:09 +0200 Subject: [PATCH 1/3] fix(sftp): handle channels concurrently to prevent 40s freeze with gvfs --- sftp/server.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sftp/server.go b/sftp/server.go index e9fa3d7f..265929b5 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -149,9 +149,11 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) erro }(requests) if srv, ok := c.manager.Get(sconn.Permissions.Extensions["uuid"]); ok { - if err := c.Handle(sconn, srv, channel); err != nil { - return err - } + go func() { + if err := c.Handle(sconn, srv, channel); err != nil { + log.WithField("error", err).Error("sftp: error handling channel") + } + }() } } return nil From 08417f012b77b4790e2ece8b6e18746abba97ea0 Mon Sep 17 00:00:00 2001 From: Aethernode Date: Sat, 20 Jun 2026 04:14:50 +0200 Subject: [PATCH 2/3] fix(sftp): Add comment --- sftp/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sftp/server.go b/sftp/server.go index 265929b5..add87e69 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -149,6 +149,7 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) erro }(requests) if srv, ok := c.manager.Get(sconn.Permissions.Extensions["uuid"]); ok { + // fix(sftp): handle channels concurrently to prevent 40s freeze with gvfs go func() { if err := c.Handle(sconn, srv, channel); err != nil { log.WithField("error", err).Error("sftp: error handling channel") From ae9f4c077fcf4d6a9b020036333ef378f75059f4 Mon Sep 17 00:00:00 2001 From: Aethernode Date: Fri, 21 Aug 2026 02:36:39 +0200 Subject: [PATCH 3/3] fix(sftp): prevent channel leaks and swallowed errors in concurrent handling - Close the SSH channel when Handle fails or no matching server is found for the connection's UUID, avoiding leaked channels that would otherwise stay open until the client disconnects. - Propagate non-EOF errors from rs.Serve() instead of always returning nil, so protocol/I/O failures are actually logged. - Track handler goroutines with a WaitGroup so AcceptInbound waits for them to finish before returning. - Re-add the "ip" field lost from error logs when channel handling moved off the main goroutine. --- sftp/server.go | 49 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/sftp/server.go b/sftp/server.go index 3ed2a943..355fa722 100644 --- a/sftp/server.go +++ b/sftp/server.go @@ -12,6 +12,7 @@ import ( "regexp" "strconv" "strings" + "sync" "emperror.dev/errors" "github.com/apex/log" @@ -125,6 +126,12 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) erro defer sconn.Close() go ssh.DiscardRequests(reqs) + // Tracks every "Handle" goroutine spawned for this connection so that we can + // wait for them to finish before this function returns. Without this, channels + // could still be in use after "sconn" is closed by the caller's deferred call. + var wg sync.WaitGroup + defer wg.Wait() + for ch := range chans { // If not a session channel we just move on because it's not something we // know how to handle at this point. @@ -148,14 +155,26 @@ func (c *SFTPServer) AcceptInbound(conn net.Conn, config *ssh.ServerConfig) erro } }(requests) - if srv, ok := c.manager.Get(sconn.Permissions.Extensions["uuid"]); ok { - // fix(sftp): handle channels concurrently to prevent 40s freeze with gvfs - go func() { - if err := c.Handle(sconn, srv, channel); err != nil { - log.WithField("error", err).Error("sftp: error handling channel") - } - }() + srv, ok := c.manager.Get(sconn.Permissions.Extensions["uuid"]) + if !ok { + // No matching server instance for this connection's UUID: nothing will + // ever consume this channel, so close it immediately to avoid leaking it. + _ = channel.Close() + continue } + + // Handle each channel concurrently so a slow or stuck client on one channel + // (e.g. gvfs holding a session open) can't block the rest of the connection. + wg.Add(1) + go func(channel ssh.Channel) { + defer wg.Done() + if err := c.Handle(sconn, srv, channel); err != nil { + _ = channel.Close() + log.WithField("error", err). + WithField("ip", conn.RemoteAddr().String()). + Error("sftp: error handling channel") + } + }(channel) } return nil } @@ -171,18 +190,26 @@ func (c *SFTPServer) Handle(conn *ssh.ServerConn, srv *server.Server, channel ss ctx := srv.Sftp().Context(handler.User()) rs := sftp.NewRequestServer(channel, handler.Handlers()) + // Signals the supervisor goroutine below to stop watching "ctx" once this + // function returns, so it doesn't linger until the server's protected-state + // context is eventually cancelled (which may be much later, or never). + done := make(chan struct{}) + defer close(done) + go func() { select { case <-ctx.Done(): srv.Log().WithField("user", conn.User()).Warn("sftp: terminating active session") _ = rs.Close() + case <-done: } }() - if err := rs.Serve(); err == io.EOF { - _ = rs.Close() + err = rs.Serve() + _ = rs.Close() + if err != nil && !errors.Is(err, io.EOF) { + return err } - return nil } @@ -261,4 +288,4 @@ func (c *SFTPServer) makeCredentialsRequest(conn ssh.ConnMetadata, t remote.Sftp // PrivateKeyPath returns the path the host private key for this server instance. func (c *SFTPServer) PrivateKeyPath() string { return path.Join(c.BasePath, ".sftp/id_ed25519") -} +} \ No newline at end of file