From d0f2727a4d2c3784cd363d4fd3c31098e8a481fa Mon Sep 17 00:00:00 2001 From: Moritz Poldrack <33086936+mpldr@users.noreply.github.com> Date: Thu, 30 Mar 2023 03:38:28 +0200 Subject: [PATCH] get rid of some panics (#1526) * client: simplify (*HostClient).do() Remove an allocation in favour of deferring a call to release the response. * client: remove panic in dialAddr Return an error instead of panicking if the user supplied a nonsensical DialFunc. * compression: remove panic on invalid compression level If a compression level exceeding gzip's boundaries is provided, fasthttp will panic. Instead it would be better to handle this error for them by limiting it to the minimum or maximum value, depending on the direction the user has exceeded the limits. Clamp the value of gzip to always be between gzip.BestSpeed and gzip.BestCompression. * peripconn: remove panic on negative connection count When a negative count is reached when unregistering a connection, a panic is caused even though data-integrity is not at risk. Replace the panic() with a simple clamp on the value to ensure the value does not exceed it's expected lower bounds. References: #1504 * compress: remove error on failed nonblocking writes Since there is no way of handling or even logging non-critical errors in stateless non-blocking writecalls, just drop them and hope the user notices and tries again. * workerPool: remove panic on redundant Start and Stop calls Instead of panicking for invalid behaviour, it's preferable to just turn the function into a noop. * http: remove panic on invalid form boundary * http: remove panic on negative reads Since bufio already panics on negative reads, it is not necessary to do so as well. If the length is zero and for some reason no error is returned, readBodyIdentity and appendBodyFixedSize now errors in these cases. Link: https://github.com/golang/go/blob/851f6fd61425c810959c7ab51e6dc86f8a63c970/src/bufio/bufio.go#L246 * fs: remove panic on negative reader count When a negative count is reached when unregistering a reader, a panic is thrown even though data-integrity is not at risk. Replace the panic() with a simple clamp on the value to ensure the value does not exceed it's expected lower bounds. * server: remove panic in favour of a segfault Panicking with "BUG: " obscures the error. As the segfault causes a panic anyway, just let the chaos unfold. * server: remove panic in favour of returning an error Writing on a timed-out response is not endangering data integrity and just fails. * chore: add comments to all panics * chore: fix minor typo --- brotli.go | 5 +---- bytesconv.go | 3 +++ client.go | 11 ++++------- compress.go | 29 +++++++++++++++++++---------- fs.go | 3 ++- http.go | 26 +++++++++++++++----------- lbclient.go | 1 + peripconn.go | 8 +++----- peripconn_test.go | 14 -------------- server.go | 17 ++++++++--------- stackless/func.go | 1 + timer.go | 1 + workerpool.go | 4 ++-- 13 files changed, 60 insertions(+), 63 deletions(-) diff --git a/brotli.go b/brotli.go index 815e4b35f1..c829c39fa7 100644 --- a/brotli.go +++ b/brotli.go @@ -138,10 +138,7 @@ func nonblockingWriteBrotli(ctxv interface{}) { ctx := ctxv.(*compressCtx) zw := acquireRealBrotliWriter(ctx.w, ctx.level) - _, err := zw.Write(ctx.p) - if err != nil { - panic(fmt.Sprintf("BUG: brotli.Writer.Write for len(p)=%d returned unexpected error: %v", len(ctx.p), err)) - } + zw.Write(ctx.p) //nolint:errcheck // no way to handle this error anyway releaseRealBrotliWriter(zw, ctx.level) } diff --git a/bytesconv.go b/bytesconv.go index 9b2ffeb9b9..809b4900f0 100644 --- a/bytesconv.go +++ b/bytesconv.go @@ -79,6 +79,7 @@ func ParseIPv4(dst net.IP, ipStr []byte) (net.IP, error) { copy(dst, net.IPv4zero) dst = dst.To4() if dst == nil { + // developer sanity-check panic("BUG: dst must not be nil") } @@ -126,6 +127,7 @@ func ParseHTTPDate(date []byte) (time.Time, error) { // AppendUint appends n to dst and returns the extended dst. func AppendUint(dst []byte, n int) []byte { if n < 0 { + // developer sanity-check panic("BUG: int must be positive") } @@ -281,6 +283,7 @@ var hexIntBufPool sync.Pool func writeHexInt(w *bufio.Writer, n int) error { if n < 0 { + // developer sanity-check panic("BUG: int must be positive") } diff --git a/client.go b/client.go index 8546a12221..9a2ef85d5a 100644 --- a/client.go +++ b/client.go @@ -1294,26 +1294,23 @@ func isIdempotent(req *Request) bool { } func (c *HostClient) do(req *Request, resp *Response) (bool, error) { - nilResp := false if resp == nil { - nilResp = true resp = AcquireResponse() + defer ReleaseResponse(resp) } ok, err := c.doNonNilReqResp(req, resp) - if nilResp { - ReleaseResponse(resp) - } - return ok, err } func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) { if req == nil { + // for debugging purposes panic("BUG: req cannot be nil") } if resp == nil { + // for debugging purposes panic("BUG: resp cannot be nil") } @@ -1994,7 +1991,7 @@ func dialAddr(addr string, dial DialFunc, dialDualStack, isTLS bool, tlsConfig * return nil, err } if conn == nil { - panic("BUG: DialFunc returned (nil, nil)") + return nil, errors.New("dialling unsuccessful. Please report this bug!") } // We assume that any conn that has the Handshake() method is a TLS conn already. diff --git a/compress.go b/compress.go index 5781c77f1b..50d381b804 100644 --- a/compress.go +++ b/compress.go @@ -66,6 +66,7 @@ func releaseFlateReader(zr io.ReadCloser) { func resetFlateReader(zr io.ReadCloser, r io.Reader) error { zrr, ok := zr.(zlib.Resetter) if !ok { + // sanity check. should only be called with a zlib.Reader panic("BUG: zlib.Reader doesn't implement zlib.Resetter???") } return zrr.Reset(r, nil) @@ -101,7 +102,14 @@ func acquireRealGzipWriter(w io.Writer, level int) *gzip.Writer { if v == nil { zw, err := gzip.NewWriterLevel(w, level) if err != nil { - panic(fmt.Sprintf("BUG: unexpected error from gzip.NewWriterLevel(%d): %v", level, err)) + // gzip.NewWriterLevel only errors for invalid + // compression levels. Clamp it to be min or max. + if level < gzip.HuffmanOnly { + level = gzip.HuffmanOnly + } else { + level = gzip.BestCompression + } + zw, _ = gzip.NewWriterLevel(w, level) } return zw } @@ -175,10 +183,7 @@ func nonblockingWriteGzip(ctxv interface{}) { ctx := ctxv.(*compressCtx) zw := acquireRealGzipWriter(ctx.w, ctx.level) - _, err := zw.Write(ctx.p) - if err != nil { - panic(fmt.Sprintf("BUG: gzip.Writer.Write for len(p)=%d returned unexpected error: %v", len(ctx.p), err)) - } + zw.Write(ctx.p) //nolint:errcheck // no way to handle this error anyway releaseRealGzipWriter(zw, ctx.level) } @@ -271,10 +276,7 @@ func nonblockingWriteDeflate(ctxv interface{}) { ctx := ctxv.(*compressCtx) zw := acquireRealDeflateWriter(ctx.w, ctx.level) - _, err := zw.Write(ctx.p) - if err != nil { - panic(fmt.Sprintf("BUG: zlib.Writer.Write for len(p)=%d returned unexpected error: %v", len(ctx.p), err)) - } + zw.Write(ctx.p) //nolint:errcheck // no way to handle this error anyway releaseRealDeflateWriter(zw, ctx.level) } @@ -379,7 +381,14 @@ func acquireRealDeflateWriter(w io.Writer, level int) *zlib.Writer { if v == nil { zw, err := zlib.NewWriterLevel(w, level) if err != nil { - panic(fmt.Sprintf("BUG: unexpected error from zlib.NewWriterLevel(%d): %v", level, err)) + // zlib.NewWriterLevel only errors for invalid + // compression levels. Clamp it to be min or max. + if level < zlib.HuffmanOnly { + level = zlib.HuffmanOnly + } else { + level = zlib.BestCompression + } + zw, _ = zlib.NewWriterLevel(w, level) } return zw } diff --git a/fs.go b/fs.go index d4ea8bd6de..1278298a80 100644 --- a/fs.go +++ b/fs.go @@ -602,7 +602,7 @@ func (ff *fsFile) decReadersCount() { ff.h.cacheLock.Lock() ff.readersCount-- if ff.readersCount < 0 { - panic("BUG: negative fsFile.readersCount!") + ff.readersCount = 0 } ff.h.cacheLock.Unlock() } @@ -1395,6 +1395,7 @@ func readFileHeader(f *os.File, compressed bool, fileEncoding string) ([]byte, e func stripLeadingSlashes(path []byte, stripSlashes int) []byte { for stripSlashes > 0 && len(path) > 0 { if path[0] != '/' { + // developer sanity-check panic("BUG: path must start with slash") } n := bytes.IndexByte(path[1:], '/') diff --git a/http.go b/http.go index ce38c1f952..b20d6658f7 100644 --- a/http.go +++ b/http.go @@ -963,7 +963,7 @@ func WriteMultipartForm(w io.Writer, f *multipart.Form, boundary string) error { // Do not care about memory allocations here, since multipart // form processing is slow. if len(boundary) == 0 { - panic("BUG: form boundary cannot be empty") + return errors.New("form boundary cannot be empty") } mw := multipart.NewWriter(w) @@ -2134,13 +2134,14 @@ func readBodyIdentity(r *bufio.Reader, maxBodySize int, dst []byte) ([]byte, err for { nn, err := r.Read(dst[offset:]) if nn <= 0 { - if err != nil { - if err == io.EOF { - return dst[:offset], nil - } + switch { + case errors.Is(err, io.EOF): + return dst[:offset], nil + case err != nil: return dst[:offset], err + default: + return dst[:offset], fmt.Errorf("bufio.Read() returned (%d, nil)", nn) } - panic(fmt.Sprintf("BUG: bufio.Read() returned (%d, nil)", nn)) } offset += nn if maxBodySize > 0 && offset > maxBodySize { @@ -2175,13 +2176,14 @@ func appendBodyFixedSize(r *bufio.Reader, dst []byte, n int) ([]byte, error) { for { nn, err := r.Read(dst[offset:]) if nn <= 0 { - if err != nil { - if err == io.EOF { - err = io.ErrUnexpectedEOF - } + switch { + case errors.Is(err, io.EOF): + return dst[:offset], io.ErrUnexpectedEOF + case err != nil: return dst[:offset], err + default: + return dst[:offset], fmt.Errorf("bufio.Read() returned (%d, nil)", nn) } - panic(fmt.Sprintf("BUG: bufio.Read() returned (%d, nil)", nn)) } offset += nn if offset == dstLen { @@ -2197,6 +2199,8 @@ type ErrBrokenChunk struct { func readBodyChunked(r *bufio.Reader, maxBodySize int, dst []byte) ([]byte, error) { if len(dst) > 0 { + // data integrity might be in danger. No idea what we received, + // but nothing we should write to. panic("BUG: expected zero-length buffer") } diff --git a/lbclient.go b/lbclient.go index 56ac1bfb44..6be2dc9bef 100644 --- a/lbclient.go +++ b/lbclient.go @@ -84,6 +84,7 @@ func (cc *LBClient) init() { cc.mu.Lock() defer cc.mu.Unlock() if len(cc.Clients) == 0 { + // developer sanity-check panic("BUG: LBClient.Clients cannot be empty") } for _, c := range cc.Clients { diff --git a/peripconn.go b/peripconn.go index c2d182714c..123c55ea36 100644 --- a/peripconn.go +++ b/peripconn.go @@ -1,7 +1,6 @@ package fasthttp import ( - "fmt" "net" "sync" ) @@ -25,17 +24,16 @@ func (cc *perIPConnCounter) Register(ip uint32) int { func (cc *perIPConnCounter) Unregister(ip uint32) { cc.lock.Lock() + defer cc.lock.Unlock() if cc.m == nil { - cc.lock.Unlock() + // developer safeguard panic("BUG: perIPConnCounter.Register() wasn't called") } n := cc.m[ip] - 1 if n < 0 { - cc.lock.Unlock() - panic(fmt.Sprintf("BUG: negative per-ip counter=%d for ip=%d", n, ip)) + n = 0 } cc.m[ip] = n - cc.lock.Unlock() } type perIPConn struct { diff --git a/peripconn_test.go b/peripconn_test.go index e2137c0f84..5571654597 100644 --- a/peripconn_test.go +++ b/peripconn_test.go @@ -25,8 +25,6 @@ func TestPerIPConnCounter(t *testing.T) { var cc perIPConnCounter - expectPanic(t, func() { cc.Unregister(123) }) - for i := 1; i < 100; i++ { if n := cc.Register(123); n != i { t.Fatalf("Unexpected counter value=%d. Expected %d", n, i) @@ -43,21 +41,9 @@ func TestPerIPConnCounter(t *testing.T) { } cc.Unregister(456) - expectPanic(t, func() { cc.Unregister(123) }) - expectPanic(t, func() { cc.Unregister(456) }) - n = cc.Register(123) if n != 1 { t.Fatalf("Unexpected counter value=%d. Expected 1", n) } cc.Unregister(123) } - -func expectPanic(t *testing.T, f func()) { - defer func() { - if r := recover(); r == nil { - t.Fatalf("Expecting panic") - } - }() - f() -} diff --git a/server.go b/server.go index 1c1c39db93..4ed82b1317 100644 --- a/server.go +++ b/server.go @@ -1929,9 +1929,6 @@ func acceptConn(s *Server, ln net.Listener, lastPerIPErrorTime *time.Time) (net. for { c, err := ln.Accept() if err != nil { - if c != nil { - panic("BUG: net.Listener returned non-nil conn and non-nil error") - } if netErr, ok := err.(net.Error); ok && netErr.Timeout() { s.logger().Printf("Timeout error when accepting new connections: %v", netErr) time.Sleep(time.Second) @@ -1943,9 +1940,6 @@ func acceptConn(s *Server, ln net.Listener, lastPerIPErrorTime *time.Time) (net. } return nil, io.EOF } - if c == nil { - panic("BUG: net.Listener returned (nil, nil)") - } if tc, ok := c.(*net.TCPConn); ok && s.TCPKeepalive { if err := tc.SetKeepAlive(s.TCPKeepalive); err != nil { @@ -2578,7 +2572,7 @@ func (ctx *RequestCtx) LastTimeoutErrorResponse() *Response { func writeResponse(ctx *RequestCtx, w *bufio.Writer) error { if ctx.timeoutResponse != nil { - panic("BUG: cannot write timed out response") + return errors.New("cannot write timed out response") } err := ctx.Response.Write(w) @@ -2596,8 +2590,8 @@ func acquireByteReader(ctxP **RequestCtx) (*bufio.Reader, error) { c := ctx.c s.releaseCtx(ctx) - // Make GC happy, so it could garbage collect ctx - // while we waiting for the next request. + // Make GC happy, so it could garbage collect ctx while we wait for the + // next request. ctx = nil *ctxP = nil @@ -2612,6 +2606,7 @@ func acquireByteReader(ctxP **RequestCtx) (*bufio.Reader, error) { return nil, io.EOF } if n != 1 { + // developer sanity-check panic("BUG: Reader must return at least one byte") } @@ -2787,19 +2782,23 @@ func (fa *fakeAddrer) LocalAddr() net.Addr { } func (fa *fakeAddrer) Read(p []byte) (int, error) { + // developer sanity-check panic("BUG: unexpected Read call") } func (fa *fakeAddrer) Write(p []byte) (int, error) { + // developer sanity-check panic("BUG: unexpected Write call") } func (fa *fakeAddrer) Close() error { + // developer sanity-check panic("BUG: unexpected Close call") } func (s *Server) releaseCtx(ctx *RequestCtx) { if ctx.timeoutResponse != nil { + // developer sanity-check panic("BUG: cannot release timed out RequestCtx") } diff --git a/stackless/func.go b/stackless/func.go index a50b3eb26f..70521e1c59 100644 --- a/stackless/func.go +++ b/stackless/func.go @@ -20,6 +20,7 @@ import ( // at the moment due to high load. func NewFunc(f func(ctx interface{})) func(ctx interface{}) bool { if f == nil { + // developer sanity-check panic("BUG: f cannot be nil") } diff --git a/timer.go b/timer.go index 885aa69511..6c06ba06e3 100644 --- a/timer.go +++ b/timer.go @@ -10,6 +10,7 @@ func initTimer(t *time.Timer, timeout time.Duration) *time.Timer { return time.NewTimer(timeout) } if t.Reset(timeout) { + // developer sanity-check panic("BUG: active timer trapped into initTimer()") } return t diff --git a/workerpool.go b/workerpool.go index 50a5c75f8c..71da1f2f9a 100644 --- a/workerpool.go +++ b/workerpool.go @@ -47,7 +47,7 @@ type workerChan struct { func (wp *workerPool) Start() { if wp.stopCh != nil { - panic("BUG: workerPool already started") + return } wp.stopCh = make(chan struct{}) stopCh := wp.stopCh @@ -72,7 +72,7 @@ func (wp *workerPool) Start() { func (wp *workerPool) Stop() { if wp.stopCh == nil { - panic("BUG: workerPool wasn't started") + return } close(wp.stopCh) wp.stopCh = nil