haproxy/include/haproxy/session.h

324 lines
9.4 KiB
C
Raw Normal View History

/*
* include/haproxy/session.h
* This file contains functions used to manage sessions.
*
* Copyright (C) 2000-2020 Willy Tarreau - w@1wt.eu
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, version 2.1
* exclusively.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _HAPROXY_SESSION_H
#define _HAPROXY_SESSION_H
#include <haproxy/api.h>
#include <haproxy/connection.h>
#include <haproxy/global-t.h>
#include <haproxy/obj_type-t.h>
#include <haproxy/pool.h>
#include <haproxy/server.h>
#include <haproxy/session-t.h>
#include <haproxy/stick_table.h>
extern struct pool_head *pool_head_session;
extern struct pool_head *pool_head_sess_srv_list;
struct session *session_new(struct proxy *fe, struct listener *li, enum obj_type *origin);
void session_free(struct session *sess);
MEDIUM: listener: allocate the connection before queuing a new connection Till now we would keep a per-thread queue of pending incoming connections for which we would store: - the listener - the accepted FD - the source address - the source address' length And these elements were first used in session_accept_fd() running on the target thread to allocate a connection and duplicate them again. Doing this induces various problems. The first one is that session_accept_fd() may only run on file descriptors and cannot be reused for QUIC. The second issue is that it induces lots of memory copies and that the listerner queue thrashes a lot of cache, consuming 64 bytes per entry. This patch changes this by allocating the connection before queueing it, and by only placing the connection's pointer into the queue. Indeed, the first two calls used to initialize the connection already store all the information above, which can be retrieved from the connection pointer alone. So we just have to pop one pointer from the target thread, and pass it to session_accept_fd() which only needs the FD for the final settings. This starts to make the accept path a bit more transport-agnostic, and saves memory and CPU cycles at the same time (1% connection rate increase was noticed with 4 threads). Thanks to dividing the accept-queue entry size from 64 to 8 bytes, its size could be increased from 256 to 1024 connections while still dividing the overall size by two. No single queue full condition was met. One minor drawback is that connection may be allocated from one thread's pool to be used into another one. But this already happens a lot with connection reuse so there is really nothing new here.
2020-10-14 11:37:17 -04:00
int session_accept_fd(struct connection *cli_conn);
int conn_complete_session(struct connection *conn);
struct task *session_expire_embryonic(struct task *t, void *context, unsigned int state);
/* Remove the refcount from the session to the tracked counters, and clear the
* pointer to ensure this is only performed once. The caller is responsible for
* ensuring that the pointer is valid first.
*/
static inline void session_store_counters(struct session *sess)
{
void *ptr;
int i;
struct stksess *ts;
for (i = 0; i < MAX_SESS_STKCTR; i++) {
struct stkctr *stkctr = &sess->stkctr[i];
ts = stkctr_entry(stkctr);
if (!ts)
continue;
ptr = stktable_data_ptr(stkctr->table, ts, STKTABLE_DT_CONN_CUR);
if (ptr) {
HA_RWLOCK_WRLOCK(STK_SESS_LOCK, &ts->lock);
if (stktable_data_cast(ptr, std_t_uint) > 0)
stktable_data_cast(ptr, std_t_uint)--;
HA_RWLOCK_WRUNLOCK(STK_SESS_LOCK, &ts->lock);
/* If data was modified, we need to touch to re-schedule sync */
stktable_touch_local(stkctr->table, ts, 0);
}
stkctr_set_entry(stkctr, NULL);
stksess_kill_if_expired(stkctr->table, ts, 1);
}
}
/* Increase the number of cumulated HTTP requests in the tracked counters */
static inline void session_inc_http_req_ctr(struct session *sess)
{
int i;
for (i = 0; i < MAX_SESS_STKCTR; i++)
stkctr_inc_http_req_ctr(&sess->stkctr[i]);
}
/* Increase the number of cumulated failed HTTP requests in the tracked
* counters. Only 4xx requests should be counted here so that we can
* distinguish between errors caused by client behaviour and other ones.
* Note that even 404 are interesting because they're generally caused by
* vulnerability scans.
*/
static inline void session_inc_http_err_ctr(struct session *sess)
{
int i;
for (i = 0; i < MAX_SESS_STKCTR; i++)
stkctr_inc_http_err_ctr(&sess->stkctr[i]);
}
MINOR: stick-tables/counters: add http_fail_cnt and http_fail_rate data types Historically we've been counting lots of client-triggered events in stick tables to help detect misbehaving ones, but we've been missing the same on the server side, and there's been repeated requests for being able to count the server errors per URL in order to precisely monitor the quality of service or even to avoid routing requests to certain dead services, which is also called "circuit breaking" nowadays. This commit introduces http_fail_cnt and http_fail_rate, which work like http_err_cnt and http_err_rate in that they respectively count events and their frequency, but they only consider server-side issues such as network errors, unparsable and truncated responses, and 5xx status codes other than 501 and 505 (since these ones are usually triggered by the client). Note that retryable errors are purposely not accounted for, so that only what the client really sees is considered. With this it becomes very simple to put some protective measures in place to perform a redirect or return an excuse page when the error rate goes beyond a certain threshold for a given URL, and give more chances to the server to recover from this condition. Typically it could look like this to bypass a URL causing more than 10 requests per second: stick-table type string len 80 size 4k expire 1m store http_fail_rate(1m) http-request track-sc0 base # track host+path, ignore query string http-request return status 503 content-type text/html \ lf-file excuse.html if { sc0_http_fail_rate gt 10 } A more advanced mechanism using gpt0 could even implement high/low rates to disable/enable the service. Reg-test converteers_ref_cnt_never_dec.vtc was updated to test it.
2021-02-10 06:07:15 -05:00
/* Increase the number of cumulated failed HTTP responses in the tracked
* counters. Only some 5xx responses should be counted here so that we can
* distinguish between server failures and errors triggered by the client
* (i.e. 501 and 505 may be triggered and must be ignored).
*/
static inline void session_inc_http_fail_ctr(struct session *sess)
{
int i;
for (i = 0; i < MAX_SESS_STKCTR; i++)
stkctr_inc_http_fail_ctr(&sess->stkctr[i]);
}
/* Remove the connection from the session list, and destroy the srv_list if it's now empty */
static inline void session_unown_conn(struct session *sess, struct connection *conn)
{
struct sess_srv_list *srv_list = NULL;
BUG_ON(objt_listener(conn->target));
BUG/MAJOR: connection: reset conn->owner when detaching from session list Baptiste reported a new crash affecting 2.3 which can be triggered when using H2 on the backend, with http-reuse always and with a tens of clients doing close only. There are a few combined cases which cause this to happen, but each time the issue is the same, an already freed session is dereferenced in session_unown_conn(). Two cases were identified to cause this: - a connection referencing a session as its owner, which is detached from the session's list and is destroyed after this session ends. The test on conn->owner before calling session_unown_conn() is not sufficent as the pointer is not null but is not valid anymore. - a connection that never goes idle and that gets killed form the mux, where session_free() is called first, then conn_free() calls session_unown_conn() which scans the just freed session for older connections. This one is only triggered with DEBUG_UAF The reason for this session to be present here is that it's needed during the connection setup, to be passed to conn_install_mux_be() to mux->init() as the owning session, but it's never deleted aftrewards. Furthermore, even conn_session_free() doesn't delete this pointer after freeing the session that lies there. Both do definitely result in a use-after-free that's more easily triggered under DEBUG_UAF. This patch makes sure that the owner is always deleted after detaching or killing the session. However it is currently not possible to clear the owner right after a synchronous init because the proxy protocol apparently needs it (a reg test checks this), and if we leave it past the connection setup with the session not attached anywhere, it's hard to catch the right moment to detach it. This means that the session may remain in conn->owner as long as the connection has never been added to nor removed from the session's idle list. Given that this patch needs to remain simple enough to be backported, instead it adds a workaround in session_unown_conn() to detect that the element is already not attached anywhere. This fix absolutely requires previous patch "CLEANUP: connection: do not use conn->owner when the session is known" otherwise the situation will be even worse, as some places used to rely on conn->owner instead of the session. The fix could theorically be backported as far as 1.8. However, the code in this area has significantly changed along versions and there are more risks of breaking working stuff than fixing real issues there. The issue was really woken up in two steps during 2.3-dev when slightly reworking the idle conns with commit 08016ab82 ("MEDIUM: connection: Add private connections synchronously in session server list") and when adding support for storing used H2 connections in the session and adding the necessary call to session_unown_conn() in the muxes. But the same test managed to crash 2.2 when built in DEBUG_UAF and patched like this, proving that we used to already leave dangling pointers behind us: | diff --git a/include/haproxy/connection.h b/include/haproxy/connection.h | index f8f235c1a..dd30b5f80 100644 | --- a/include/haproxy/connection.h | +++ b/include/haproxy/connection.h | @@ -458,6 +458,10 @@ static inline void conn_free(struct connection *conn) | sess->idle_conns--; | session_unown_conn(sess, conn); | } | + else { | + struct session *sess = conn->owner; | + BUG_ON(sess && sess->origin != &conn->obj_type); | + } | | sockaddr_free(&conn->src); | sockaddr_free(&conn->dst); It's uncertain whether an existing code path there can lead to dereferencing conn->owner when it's bad, though certain suspicious memory corruption bugs make one think it's a likely candidate. The patch should not be hard to adapt there. Backports to 2.1 and older are left to the appreciation of the person doing the backport. A reproducer consists in this: global nbthread 1 listen l bind :9000 mode http http-reuse always server s 127.0.0.1:8999 proto h2 frontend f bind :8999 proto h2 mode http http-request return status 200 Then this will make it crash within 2-3 seconds: $ h1load -e -r 1 -c 10 http://0:9000/ If it does not, it might be that DEBUG_UAF was not used (it's harder then) and it might be useful to restart.
2020-11-20 11:22:44 -05:00
/* WT: this currently is a workaround for an inconsistency between
* the link status of the connection in the session list and the
* connection's owner. This should be removed as soon as all this
* is addressed. Right now it's possible to enter here with a non-null
* conn->owner that points to a dead session, but in this case the
* element is not linked.
*/
if (!LIST_INLIST(&conn->session_list))
BUG/MAJOR: connection: reset conn->owner when detaching from session list Baptiste reported a new crash affecting 2.3 which can be triggered when using H2 on the backend, with http-reuse always and with a tens of clients doing close only. There are a few combined cases which cause this to happen, but each time the issue is the same, an already freed session is dereferenced in session_unown_conn(). Two cases were identified to cause this: - a connection referencing a session as its owner, which is detached from the session's list and is destroyed after this session ends. The test on conn->owner before calling session_unown_conn() is not sufficent as the pointer is not null but is not valid anymore. - a connection that never goes idle and that gets killed form the mux, where session_free() is called first, then conn_free() calls session_unown_conn() which scans the just freed session for older connections. This one is only triggered with DEBUG_UAF The reason for this session to be present here is that it's needed during the connection setup, to be passed to conn_install_mux_be() to mux->init() as the owning session, but it's never deleted aftrewards. Furthermore, even conn_session_free() doesn't delete this pointer after freeing the session that lies there. Both do definitely result in a use-after-free that's more easily triggered under DEBUG_UAF. This patch makes sure that the owner is always deleted after detaching or killing the session. However it is currently not possible to clear the owner right after a synchronous init because the proxy protocol apparently needs it (a reg test checks this), and if we leave it past the connection setup with the session not attached anywhere, it's hard to catch the right moment to detach it. This means that the session may remain in conn->owner as long as the connection has never been added to nor removed from the session's idle list. Given that this patch needs to remain simple enough to be backported, instead it adds a workaround in session_unown_conn() to detect that the element is already not attached anywhere. This fix absolutely requires previous patch "CLEANUP: connection: do not use conn->owner when the session is known" otherwise the situation will be even worse, as some places used to rely on conn->owner instead of the session. The fix could theorically be backported as far as 1.8. However, the code in this area has significantly changed along versions and there are more risks of breaking working stuff than fixing real issues there. The issue was really woken up in two steps during 2.3-dev when slightly reworking the idle conns with commit 08016ab82 ("MEDIUM: connection: Add private connections synchronously in session server list") and when adding support for storing used H2 connections in the session and adding the necessary call to session_unown_conn() in the muxes. But the same test managed to crash 2.2 when built in DEBUG_UAF and patched like this, proving that we used to already leave dangling pointers behind us: | diff --git a/include/haproxy/connection.h b/include/haproxy/connection.h | index f8f235c1a..dd30b5f80 100644 | --- a/include/haproxy/connection.h | +++ b/include/haproxy/connection.h | @@ -458,6 +458,10 @@ static inline void conn_free(struct connection *conn) | sess->idle_conns--; | session_unown_conn(sess, conn); | } | + else { | + struct session *sess = conn->owner; | + BUG_ON(sess && sess->origin != &conn->obj_type); | + } | | sockaddr_free(&conn->src); | sockaddr_free(&conn->dst); It's uncertain whether an existing code path there can lead to dereferencing conn->owner when it's bad, though certain suspicious memory corruption bugs make one think it's a likely candidate. The patch should not be hard to adapt there. Backports to 2.1 and older are left to the appreciation of the person doing the backport. A reproducer consists in this: global nbthread 1 listen l bind :9000 mode http http-reuse always server s 127.0.0.1:8999 proto h2 frontend f bind :8999 proto h2 mode http http-request return status 200 Then this will make it crash within 2-3 seconds: $ h1load -e -r 1 -c 10 http://0:9000/ If it does not, it might be that DEBUG_UAF was not used (it's harder then) and it might be useful to restart.
2020-11-20 11:22:44 -05:00
return;
if (conn->flags & CO_FL_SESS_IDLE)
sess->idle_conns--;
LIST_DEL_INIT(&conn->session_list);
BUG/MAJOR: connection: reset conn->owner when detaching from session list Baptiste reported a new crash affecting 2.3 which can be triggered when using H2 on the backend, with http-reuse always and with a tens of clients doing close only. There are a few combined cases which cause this to happen, but each time the issue is the same, an already freed session is dereferenced in session_unown_conn(). Two cases were identified to cause this: - a connection referencing a session as its owner, which is detached from the session's list and is destroyed after this session ends. The test on conn->owner before calling session_unown_conn() is not sufficent as the pointer is not null but is not valid anymore. - a connection that never goes idle and that gets killed form the mux, where session_free() is called first, then conn_free() calls session_unown_conn() which scans the just freed session for older connections. This one is only triggered with DEBUG_UAF The reason for this session to be present here is that it's needed during the connection setup, to be passed to conn_install_mux_be() to mux->init() as the owning session, but it's never deleted aftrewards. Furthermore, even conn_session_free() doesn't delete this pointer after freeing the session that lies there. Both do definitely result in a use-after-free that's more easily triggered under DEBUG_UAF. This patch makes sure that the owner is always deleted after detaching or killing the session. However it is currently not possible to clear the owner right after a synchronous init because the proxy protocol apparently needs it (a reg test checks this), and if we leave it past the connection setup with the session not attached anywhere, it's hard to catch the right moment to detach it. This means that the session may remain in conn->owner as long as the connection has never been added to nor removed from the session's idle list. Given that this patch needs to remain simple enough to be backported, instead it adds a workaround in session_unown_conn() to detect that the element is already not attached anywhere. This fix absolutely requires previous patch "CLEANUP: connection: do not use conn->owner when the session is known" otherwise the situation will be even worse, as some places used to rely on conn->owner instead of the session. The fix could theorically be backported as far as 1.8. However, the code in this area has significantly changed along versions and there are more risks of breaking working stuff than fixing real issues there. The issue was really woken up in two steps during 2.3-dev when slightly reworking the idle conns with commit 08016ab82 ("MEDIUM: connection: Add private connections synchronously in session server list") and when adding support for storing used H2 connections in the session and adding the necessary call to session_unown_conn() in the muxes. But the same test managed to crash 2.2 when built in DEBUG_UAF and patched like this, proving that we used to already leave dangling pointers behind us: | diff --git a/include/haproxy/connection.h b/include/haproxy/connection.h | index f8f235c1a..dd30b5f80 100644 | --- a/include/haproxy/connection.h | +++ b/include/haproxy/connection.h | @@ -458,6 +458,10 @@ static inline void conn_free(struct connection *conn) | sess->idle_conns--; | session_unown_conn(sess, conn); | } | + else { | + struct session *sess = conn->owner; | + BUG_ON(sess && sess->origin != &conn->obj_type); | + } | | sockaddr_free(&conn->src); | sockaddr_free(&conn->dst); It's uncertain whether an existing code path there can lead to dereferencing conn->owner when it's bad, though certain suspicious memory corruption bugs make one think it's a likely candidate. The patch should not be hard to adapt there. Backports to 2.1 and older are left to the appreciation of the person doing the backport. A reproducer consists in this: global nbthread 1 listen l bind :9000 mode http http-reuse always server s 127.0.0.1:8999 proto h2 frontend f bind :8999 proto h2 mode http http-request return status 200 Then this will make it crash within 2-3 seconds: $ h1load -e -r 1 -c 10 http://0:9000/ If it does not, it might be that DEBUG_UAF was not used (it's harder then) and it might be useful to restart.
2020-11-20 11:22:44 -05:00
conn->owner = NULL;
list_for_each_entry(srv_list, &sess->srv_list, srv_list) {
if (srv_list->target == conn->target) {
if (LIST_ISEMPTY(&srv_list->conn_list)) {
LIST_DELETE(&srv_list->srv_list);
pool_free(pool_head_sess_srv_list, srv_list);
}
break;
}
}
}
/* Add the connection <conn> to the server list of the session <sess>. This
* function is called only if the connection is private. Nothing is performed if
* the connection is already in the session sever list or if the session does
* not own the connection.
*/
static inline int session_add_conn(struct session *sess, struct connection *conn, void *target)
{
struct sess_srv_list *srv_list = NULL;
int found = 0;
BUG_ON(objt_listener(conn->target));
/* Already attach to the session or not the connection owner */
if (!LIST_ISEMPTY(&conn->session_list) || (conn->owner && conn->owner != sess))
return 1;
list_for_each_entry(srv_list, &sess->srv_list, srv_list) {
if (srv_list->target == target) {
found = 1;
break;
}
}
if (!found) {
/* The session has no connection for the server, create a new entry */
srv_list = pool_alloc(pool_head_sess_srv_list);
if (!srv_list)
return 0;
srv_list->target = target;
LIST_INIT(&srv_list->conn_list);
LIST_APPEND(&sess->srv_list, &srv_list->srv_list);
}
LIST_APPEND(&srv_list->conn_list, &conn->session_list);
return 1;
}
/* Returns 0 if the session can keep the idle conn, -1 if it was destroyed. The
* connection must be private.
*/
static inline int session_check_idle_conn(struct session *sess, struct connection *conn)
{
/* Another session owns this connection */
if (conn->owner != sess)
return 0;
if (sess->idle_conns >= sess->fe->max_out_conns) {
session_unown_conn(sess, conn);
conn->owner = NULL;
conn->flags &= ~CO_FL_SESS_IDLE;
conn->mux->destroy(conn->ctx);
return -1;
} else {
conn->flags |= CO_FL_SESS_IDLE;
sess->idle_conns++;
}
return 0;
}
/* Look for an available connection matching the target <target> in the server
* list of the session <sess>. It returns a connection if found. Otherwise it
* returns NULL.
*/
static inline struct connection *session_get_conn(struct session *sess, void *target, int64_t hash)
{
struct connection *srv_conn = NULL;
struct sess_srv_list *srv_list;
list_for_each_entry(srv_list, &sess->srv_list, srv_list) {
if (srv_list->target == target) {
list_for_each_entry(srv_conn, &srv_list->conn_list, session_list) {
if ((srv_conn->hash_node && srv_conn->hash_node->node.key == hash) &&
srv_conn->mux &&
(srv_conn->mux->avail_streams(srv_conn) > 0) &&
BUG/MEDIUM: session: only retrieve ready idle conn from session A bug was introduced by the early insertion of idle connections at the end of connect_server. It is possible to reuse a connection not yet ready waiting for an handshake (for example with proxy protocol or ssl). A wrong duplicate xprt_handshake_io_cb tasklet is thus registered as a side-effect. This triggers the BUG_ON statement of xprt_handshake_subscribe : BUG_ON(ctx->subs && ctx->subs != es); To counter this, a check is now present in session_get_conn to only return a connection without the flag CO_FL_WAIT_XPRT. This might cause sometimes the creation of dedicated server connections when in theory reuse could have been used, but probably only occurs rarely in real condition. This behavior is present since commit : MEDIUM: connection: Add private connections synchronously in session server list It could also be further exagerated by : MEDIUM: backend: add reused conn to sess if mux marked as HOL blocking It can be backported up to 2.3. NOTE : This bug seems to be only reproducible with mode tcp, for an unknown reason. However, reuse should never happen when not in http mode. This improper behavior will be the subject of a dedicated patch. This bug can easily be reproducible with the following config (a webserver is required to accept proxy protocol on port 31080) : global defaults mode tcp timeout connect 1s timeout server 1s timeout client 1s listen li bind 0.0.0.0:4444 server bla1 127.0.0.1:31080 check send-proxy-v2 with the inject client : $ inject -u 10000 -d 10 -G 127.0.0.1:4444 This should fix the github issue #1058.
2021-01-26 08:14:37 -05:00
!(srv_conn->flags & CO_FL_WAIT_XPRT)) {
if (srv_conn->flags & CO_FL_SESS_IDLE) {
srv_conn->flags &= ~CO_FL_SESS_IDLE;
sess->idle_conns--;
}
goto end;
}
}
srv_conn = NULL; /* No available connection found */
goto end;
}
}
end:
return srv_conn;
}
/* Returns the source address of the session and fallbacks on the client
* connection if not set. It returns a const address on success or NULL on
* failure.
*/
static inline const struct sockaddr_storage *sess_src(struct session *sess)
{
struct connection *cli_conn = objt_conn(sess->origin);
if (sess->src)
return sess->src;
if (cli_conn && conn_get_src(cli_conn))
return conn_src(cli_conn);
return NULL;
}
/* Returns the destination address of the session and fallbacks on the client
* connection if not set. It returns a const address on success or NULL on
* failure.
*/
static inline const struct sockaddr_storage *sess_dst(struct session *sess)
{
struct connection *cli_conn = objt_conn(sess->origin);
if (sess->dst)
return sess->dst;
if (cli_conn && conn_get_dst(cli_conn))
return conn_dst(cli_conn);
return NULL;
}
/* Retrieves the source address of the session <sess>. Returns non-zero on
* success or zero on failure. The operation is only performed once and the
* address is stored in the session for future use. On the first call, the
* session source address is copied from the client connection one.
*/
static inline int sess_get_src(struct session *sess)
{
struct connection *cli_conn = objt_conn(sess->origin);
const struct sockaddr_storage *src = NULL;
if (sess->src)
return 1;
if (cli_conn && conn_get_src(cli_conn))
src = conn_src(cli_conn);
if (!src)
return 0;
if (!sockaddr_alloc(&sess->src, src, sizeof(*src)))
return 0;
return 1;
}
/* Retrieves the destination address of the session <sess>. Returns non-zero on
* success or zero on failure. The operation is only performed once and the
* address is stored in the session for future use. On the first call, the
* session destination address is copied from the client connection one.
*/
static inline int sess_get_dst(struct session *sess)
{
struct connection *cli_conn = objt_conn(sess->origin);
const struct sockaddr_storage *dst = NULL;
if (sess->dst)
return 1;
if (cli_conn && conn_get_dst(cli_conn))
dst = conn_dst(cli_conn);
if (!dst)
return 0;
if (!sockaddr_alloc(&sess->dst, dst, sizeof(*dst)))
return 0;
return 1;
}
#endif /* _HAPROXY_SESSION_H */
/*
* Local variables:
* c-indent-level: 8
* c-basic-offset: 8
* End:
*/