imapserver: Prevent spurious unhandled panics for connections with compress=deflate that break

Writing to a connection goes through the flate library to compress. That writes
the compressed bytes to the underlying connection. But that underlying
connection is wrapped to raise a panic with an i/o error instead of returning a
normal error.  Jumping out of flate leaves the internal state of the compressor
in undefined state. So far so good. But as part of cleaning up the connection,
we could try to flush output again. Specifically: If we were writing user data,
we had switched from tracing of protocol data to tracing of user data, and we
registered a defer that restored the tracing kind and flushed (to ensure data
was traced at the right level). That flush would cause a write into the
compressor again, which could panic with an out of bounds slice access due to
its inconsistent internal state.

This fix prevents that compressor panic in two ways:

1. We wrap the flate.Writer with a moxio.FlateWriter that keeps track of
   whether a panic came out of an operation on it. If so, any further operation
   raises the same panic. This prevents access to the inconsistent internal flate
   state entirely.
2. Once we raise an i/o error, we mark the connection as broken and that makes
   flushes a no-op.
This commit is contained in:
Mechiel Lukkien
2025-02-26 10:50:04 +01:00
parent ea55c85938
commit 17de90e29d
7 changed files with 157 additions and 26 deletions

View File

@ -309,6 +309,12 @@ func (tc *testconn) waitDone() {
}
func (tc *testconn) close() {
defer func() {
if unhandledPanics.Swap(0) > 0 {
tc.t.Fatalf("handled panic in server")
}
}()
if tc.account == nil {
// Already closed, we are not strict about closing multiple times.
return
@ -317,7 +323,9 @@ func (tc *testconn) close() {
tc.check(err, "close account")
// no account.CheckClosed(), the tests open accounts multiple times.
tc.account = nil
tc.client.Close()
if tc.client != nil {
tc.client.Close()
}
tc.serverConn.Close()
tc.waitDone()
if tc.switchStop != nil {
@ -381,9 +389,9 @@ func (c namedConn) RemoteAddr() net.Addr {
func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientConfig *tls.Config, allowLoginWithoutTLS, noCloseSwitchboard, setPassword bool, accname string, afterInit func() error) *testconn {
limitersInit() // Reset rate limiters.
mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
mox.MustLoadConfig(true, false)
if first {
mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
mox.MustLoadConfig(true, false)
store.Close() // May not be open, we ignore error.
os.RemoveAll("../testdata/imap/data")
err := store.Init(ctxbg)
@ -418,7 +426,15 @@ func startArgsMore(t *testing.T, first, immediateTLS bool, serverConfig, clientC
tcheck(t, err, "fileconn")
err = f.Close()
tcheck(t, err, "close file for conn")
return namedConn{fc}
// Small read/write buffers, for detecting closed/broken connections quickly.
uc := fc.(*net.UnixConn)
err = uc.SetReadBuffer(512)
tcheck(t, err, "set read buffer")
uc.SetWriteBuffer(512)
tcheck(t, err, "set write buffer")
return namedConn{uc}
}
serverConn := xfdconn(fds[0], "server")
clientConn := xfdconn(fds[1], "client")