[PATCH 2 of 4] Upstream: fixed usage of closed sockets with filter finalization

Maxim Dounin mdounin at mdounin.ru
Tue Jan 30 03:07:53 UTC 2024


Hello!

On Mon, Jan 29, 2024 at 05:21:43PM +0400, Sergey Kandaurov wrote:

> 
> > On 29 Jan 2024, at 10:43, Maxim Dounin <mdounin at mdounin.ru> wrote:
> > 
> > Hello!
> > 
> > On Fri, Jan 26, 2024 at 04:26:00PM +0400, Sergey Kandaurov wrote:
> > 
> >>> On 27 Nov 2023, at 06:50, Maxim Dounin <mdounin at mdounin.ru> wrote:
> >>> 
> >>> # HG changeset patch
> >>> # User Maxim Dounin <mdounin at mdounin.ru>
> >>> # Date 1701049758 -10800
> >>> #      Mon Nov 27 04:49:18 2023 +0300
> >>> # Node ID faf0b9defc76b8683af466f8a950c2c241382970
> >>> # Parent  a5e39e9d1f4c84dcbe6a2f9e079372a3d63aef0b
> >>> Upstream: fixed usage of closed sockets with filter finalization.
> >>> 
> >>> When filter finalization is triggered when working with an upstream server,
> >>> and error_page redirects request processing to some simple handler,
> >>> ngx_http_request_finalize() triggers request termination when the response
> >>> is sent.  In particular, via the upstream cleanup handler, nginx will close
> >>> the upstream connection and the corresponding socket.
> >>> 
> >>> Still, this can happen to be with ngx_event_pipe() on stack.  While
> >>> the code will set p->downstream_error due to NGX_ERROR returned from the
> >>> output filter chain by filter finalization, otherwise the error will be
> >>> ignored till control returns to ngx_http_upstream_process_request().
> >>> And event pipe might try reading from the (already closed) socket, resulting
> >>> in "readv() failed (9: Bad file descriptor) while reading upstream" errors
> >>> (or even segfaults with SSL).
> >>> 
> >>> Such errors were seen with the following configuration:
> >>> 
> >>>   location /t2 {
> >>>       proxy_pass http://127.0.0.1:8080/big;
> >>> 
> >>>       image_filter_buffer 10m;
> >>>       image_filter   resize  150 100;
> >>>       error_page     415   = /empty;
> >>>   }
> >>> 
> >>>   location /empty {
> >>>       return 204;
> >>>   }
> >>> 
> >>>   location /big {
> >>>       # big enough static file
> >>>   }
> >>> 
> >>> Fix is to set p->upstream_error in ngx_http_upstream_finalize_request(),
> >>> so the existing checks in ngx_event_pipe_read_upstream() will prevent
> >>> further reading from the closed upstream connection.
> >>> 
> >>> Similarly, p->upstream_error is now checked when handling events at
> >>> ngx_event_pipe() exit, as checking p->upstream->fd is not enough if
> >>> keepalive upstream connections are being used and the connection was
> >>> saved to cache during request termination.
> >>> 
> >> 
> >> Setting p->upstream_error in ngx_http_upstream_finalize_request()
> >> may look suspicious, because it is used to be set on connection errors
> >> such as upstream timeout or recv error, or, as a recently introduced
> >> exception in the fastcgi module, - also when the FastCGI record ends
> >> prematurely, before receiving all the expected content.
> >> But technically I think this is quite correct, because we no longer
> >> want to receive further data, and also (and you mention this in the
> >> commit log) this repeats closing an upstream connection socket in
> >> the same place in ngx_http_upstream_finalize_request().
> >> So I think it should be fine.
> > 
> > The biggest concern I personally see here is with the added 
> > p->upstream_error check at ngx_event_pipe() exit.  If there is a 
> > real upstream error, such as when the connection is reset by the 
> > upstream server, and if we want the pipe to be active for some 
> > time (for example, if we want it to continue writing to the 
> > downstream connection), there will be no ngx_handle_read_event() 
> > call.  For level-triggered event methods this means that the read 
> > event for the upstream connection will be generated again and 
> > again.
> > 
> > This shouldn't be the problem for existing ngx_event_pipe() uses 
> > though, as p->upstream_error is anyway triggers 
> > ngx_http_upstream_finalize_request().
> > 
> > Still, we can consider introducing a separate flag, such as 
> > p->upstream_closed, or clearing p->upstream, and checking these in 
> > ngx_event_pipe() instead.  This probably would be a more clear 
> > solution.
> > 
> > Updated patch below:
> > 
> > # HG changeset patch
> > # User Maxim Dounin <mdounin at mdounin.ru>
> > # Date 1706510064 -10800
> > #      Mon Jan 29 09:34:24 2024 +0300
> > # Node ID 4a91a03dcd8df0652884ed6ebe9f7437ce82fd26
> > # Parent  7b630f6487068f7cc9dd83762fb4ea39f2f340e9
> > Upstream: fixed usage of closed sockets with filter finalization.
> > 
> > When filter finalization is triggered when working with an upstream server,
> > and error_page redirects request processing to some simple handler,
> > ngx_http_request_finalize() triggers request termination when the response
> > is sent.  In particular, via the upstream cleanup handler, nginx will close
> > the upstream connection and the corresponding socket.
> > 
> > Still, this can happen to be with ngx_event_pipe() on stack.  While
> > the code will set p->downstream_error due to NGX_ERROR returned from the
> > output filter chain by filter finalization, otherwise the error will be
> > ignored till control returns to ngx_http_upstream_process_request().
> > And event pipe might try reading from the (already closed) socket, resulting
> > in "readv() failed (9: Bad file descriptor) while reading upstream" errors
> > (or even segfaults with SSL).
> > 
> > Such errors were seen with the following configuration:
> > 
> >    location /t2 {
> >        proxy_pass http://127.0.0.1:8080/big;
> > 
> >        image_filter_buffer 10m;
> >        image_filter   resize  150 100;
> >        error_page     415   = /empty;
> >    }
> > 
> >    location /empty {
> >        return 204;
> >    }
> > 
> >    location /big {
> >        # big enough static file
> >    }
> > 
> > Fix is to clear p->upstream in ngx_http_upstream_finalize_request(),
> > and ensure that p->upstream is checked in ngx_event_pipe_read_upstream()
> > and when handling events at ngx_event_pipe() exit.
> > 
> > diff --git a/src/event/ngx_event_pipe.c b/src/event/ngx_event_pipe.c
> > --- a/src/event/ngx_event_pipe.c
> > +++ b/src/event/ngx_event_pipe.c
> > @@ -57,7 +57,9 @@ ngx_event_pipe(ngx_event_pipe_t *p, ngx_
> >         do_write = 1;
> >     }
> > 
> > -    if (p->upstream->fd != (ngx_socket_t) -1) {
> > +    if (p->upstream
> > +        && p->upstream->fd != (ngx_socket_t) -1)
> > +    {
> >         rev = p->upstream->read;
> > 
> >         flags = (rev->eof || rev->error) ? NGX_CLOSE_EVENT : 0;
> > @@ -108,7 +110,9 @@ ngx_event_pipe_read_upstream(ngx_event_p
> >     ngx_msec_t    delay;
> >     ngx_chain_t  *chain, *cl, *ln;
> > 
> > -    if (p->upstream_eof || p->upstream_error || p->upstream_done) {
> > +    if (p->upstream_eof || p->upstream_error || p->upstream_done
> > +        || p->upstream == NULL)
> > +    {
> >         return NGX_OK;
> >     }
> > 
> > diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c
> > --- a/src/http/ngx_http_upstream.c
> > +++ b/src/http/ngx_http_upstream.c
> > @@ -4561,6 +4561,10 @@ ngx_http_upstream_finalize_request(ngx_h
> > 
> >     u->peer.connection = NULL;
> > 
> > +    if (u->pipe) {
> > +        u->pipe->upstream = NULL;
> > +    }
> > +
> >     if (u->pipe && u->pipe->temp_file) {
> >         ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
> >                        "http upstream temp fd: %d",
> > 
> 
> Indeed, this fix looks more isolated, I like it.

Pushed to http://mdounin.ru/hg/nginx, thanks for the review.

-- 
Maxim Dounin
http://mdounin.ru/


More information about the nginx-devel mailing list