commit 937604b4cfccddd607b8d4883815c4e3f9ab70d0 Author: Willy Tarreau Date: Wed Jul 24 16:45:02 2019 +0200 BUG/MEDIUM: protocols: add a global lock for the init/deinit stuff Dragan Dosen found that the listeners lock is not sufficient to protect the listeners list when proxies are stopping because the listeners are also unlinked from the protocol list, and under certain situations like bombing with soft-stop signals or shutting down many frontends in parallel from multiple CLI connections, it could be possible to provoke multiple instances of delete_listener() to be called in parallel for different listeners, thus corrupting the protocol lists. Such operations are pretty rare, they are performed once per proxy upon startup and once per proxy on shut down. Thus there is no point trying to optimize anything and we can use a global lock to protect the protocol lists during these manipulations. This fix (or a variant) will have to be backported as far as 1.8. (cherry picked from commit daacf3664506d56a1f3b050ccba504886a18b12a) Signed-off-by: Christopher Faulet diff --git a/include/proto/protocol.h b/include/proto/protocol.h index 7bbebb8e..f25f77f0 100644 --- a/include/proto/protocol.h +++ b/include/proto/protocol.h @@ -23,9 +23,11 @@ #define _PROTO_PROTOCOL_H #include +#include #include extern struct protocol *__protocol_by_family[AF_CUST_MAX]; +__decl_hathreads(extern HA_SPINLOCK_T proto_lock); /* Registers the protocol */ void protocol_register(struct protocol *proto); diff --git a/include/types/protocol.h b/include/types/protocol.h index 1d3404b9..f38baeb9 100644 --- a/include/types/protocol.h +++ b/include/types/protocol.h @@ -80,9 +80,9 @@ struct protocol { int (*pause)(struct listener *l); /* temporarily pause this listener for a soft restart */ void (*add)(struct listener *l, int port); /* add a listener for this protocol and port */ - struct list listeners; /* list of listeners using this protocol */ - int nb_listeners; /* number of listeners */ - struct list list; /* list of registered protocols */ + struct list listeners; /* list of listeners using this protocol (under proto_lock) */ + int nb_listeners; /* number of listeners (under proto_lock) */ + struct list list; /* list of registered protocols (under proto_lock) */ }; #define CONNECT_HAS_DATA 0x00000001 /* There's data available to be sent */ diff --git a/src/listener.c b/src/listener.c index 40a774ed..b5fe2ac2 100644 --- a/src/listener.c +++ b/src/listener.c @@ -433,6 +433,9 @@ static void limit_listener(struct listener *l, struct list *list) * used as a protocol's generic enable_all() primitive, for use after the * fork(). It puts the listeners into LI_READY or LI_FULL states depending on * their number of connections. It always returns ERR_NONE. + * + * Must be called with proto_lock held. + * */ int enable_all_listeners(struct protocol *proto) { @@ -447,6 +450,9 @@ int enable_all_listeners(struct protocol *proto) * the polling lists when they are in the LI_READY or LI_FULL states. It is * intended to be used as a protocol's generic disable_all() primitive. It puts * the listeners into LI_LISTEN, and always returns ERR_NONE. + * + * Must be called with proto_lock held. + * */ int disable_all_listeners(struct protocol *proto) { @@ -516,6 +522,9 @@ void unbind_listener_no_close(struct listener *listener) /* This function closes all listening sockets bound to the protocol , * and the listeners end in LI_ASSIGNED state if they were higher. It does not * detach them from the protocol. It always returns ERR_NONE. + * + * Must be called with proto_lock held. + * */ int unbind_all_listeners(struct protocol *proto) { @@ -580,14 +589,19 @@ int create_listeners(struct bind_conf *bc, const struct sockaddr_storage *ss, * number of listeners is updated, as well as the global number of listeners * and jobs. Note that the listener must have previously been unbound. This * is the generic function to use to remove a listener. + * + * Will grab the proto_lock. + * */ void delete_listener(struct listener *listener) { HA_SPIN_LOCK(LISTENER_LOCK, &listener->lock); if (listener->state == LI_ASSIGNED) { listener->state = LI_INIT; + HA_SPIN_LOCK(PROTO_LOCK, &proto_lock); LIST_DEL(&listener->proto_list); listener->proto->nb_listeners--; + HA_SPIN_UNLOCK(PROTO_LOCK, &proto_lock); _HA_ATOMIC_SUB(&jobs, 1); _HA_ATOMIC_SUB(&listeners, 1); } diff --git a/src/proto_sockpair.c b/src/proto_sockpair.c index a4faa370..e7dd670d 100644 --- a/src/proto_sockpair.c +++ b/src/proto_sockpair.c @@ -80,6 +80,9 @@ INITCALL1(STG_REGISTER, protocol_register, &proto_sockpair); /* Add to the list of sockpair listeners (port is ignored). The * listener's state is automatically updated from LI_INIT to LI_ASSIGNED. * The number of listeners for the protocol is updated. + * + * Must be called with proto_lock held. + * */ static void sockpair_add_listener(struct listener *listener, int port) { @@ -97,6 +100,8 @@ static void sockpair_add_listener(struct listener *listener, int port) * loose them across the fork(). A call to uxst_enable_listeners() is needed * to complete initialization. * + * Must be called with proto_lock held. + * * The return value is composed from ERR_NONE, ERR_RETRYABLE and ERR_FATAL. */ static int sockpair_bind_listeners(struct protocol *proto, char *errmsg, int errlen) diff --git a/src/proto_tcp.c b/src/proto_tcp.c index 64ffb83c..bcbe27a7 100644 --- a/src/proto_tcp.c +++ b/src/proto_tcp.c @@ -1103,6 +1103,9 @@ int tcp_bind_listener(struct listener *listener, char *errmsg, int errlen) * The sockets will be registered but not added to any fd_set, in order not to * loose them across the fork(). A call to enable_all_listeners() is needed * to complete initialization. The return value is composed from ERR_*. + * + * Must be called with proto_lock held. + * */ static int tcp_bind_listeners(struct protocol *proto, char *errmsg, int errlen) { @@ -1121,6 +1124,9 @@ static int tcp_bind_listeners(struct protocol *proto, char *errmsg, int errlen) /* Add to the list of tcpv4 listeners, on port . The * listener's state is automatically updated from LI_INIT to LI_ASSIGNED. * The number of listeners for the protocol is updated. + * + * Must be called with proto_lock held. + * */ static void tcpv4_add_listener(struct listener *listener, int port) { @@ -1136,6 +1142,9 @@ static void tcpv4_add_listener(struct listener *listener, int port) /* Add to the list of tcpv6 listeners, on port . The * listener's state is automatically updated from LI_INIT to LI_ASSIGNED. * The number of listeners for the protocol is updated. + * + * Must be called with proto_lock held. + * */ static void tcpv6_add_listener(struct listener *listener, int port) { diff --git a/src/proto_uxst.c b/src/proto_uxst.c index 66093af6..7263240f 100644 --- a/src/proto_uxst.c +++ b/src/proto_uxst.c @@ -379,6 +379,9 @@ static int uxst_unbind_listener(struct listener *listener) /* Add to the list of unix stream listeners (port is ignored). The * listener's state is automatically updated from LI_INIT to LI_ASSIGNED. * The number of listeners for the protocol is updated. + * + * Must be called with proto_lock held. + * */ static void uxst_add_listener(struct listener *listener, int port) { @@ -594,6 +597,8 @@ static int uxst_connect_server(struct connection *conn, int flags) * loose them across the fork(). A call to uxst_enable_listeners() is needed * to complete initialization. * + * Must be called with proto_lock held. + * * The return value is composed from ERR_NONE, ERR_RETRYABLE and ERR_FATAL. */ static int uxst_bind_listeners(struct protocol *proto, char *errmsg, int errlen) @@ -613,6 +618,9 @@ static int uxst_bind_listeners(struct protocol *proto, char *errmsg, int errlen) /* This function stops all listening UNIX sockets bound to the protocol * . It does not detaches them from the protocol. * It always returns ERR_NONE. + * + * Must be called with proto_lock held. + * */ static int uxst_unbind_listeners(struct protocol *proto) { diff --git a/src/protocol.c b/src/protocol.c index 96e01c82..ac45cf2e 100644 --- a/src/protocol.c +++ b/src/protocol.c @@ -18,18 +18,26 @@ #include #include -#include +#include /* List head of all registered protocols */ static struct list protocols = LIST_HEAD_INIT(protocols); struct protocol *__protocol_by_family[AF_CUST_MAX] = { }; +/* This is the global spinlock we may need to register/unregister listeners or + * protocols. Its main purpose is in fact to serialize the rare stop/deinit() + * phases. + */ +__decl_spinlock(proto_lock); + /* Registers the protocol */ void protocol_register(struct protocol *proto) { + HA_SPIN_LOCK(PROTO_LOCK, &proto_lock); LIST_ADDQ(&protocols, &proto->list); if (proto->sock_domain >= 0 && proto->sock_domain < AF_CUST_MAX) __protocol_by_family[proto->sock_domain] = proto; + HA_SPIN_UNLOCK(PROTO_LOCK, &proto_lock); } /* Unregisters the protocol . Note that all listeners must have @@ -37,8 +45,10 @@ void protocol_register(struct protocol *proto) */ void protocol_unregister(struct protocol *proto) { + HA_SPIN_LOCK(PROTO_LOCK, &proto_lock); LIST_DEL(&proto->list); LIST_INIT(&proto->list); + HA_SPIN_UNLOCK(PROTO_LOCK, &proto_lock); } /* binds all listeners of all registered protocols. Returns a composition @@ -50,6 +60,7 @@ int protocol_bind_all(char *errmsg, int errlen) int err; err = 0; + HA_SPIN_LOCK(PROTO_LOCK, &proto_lock); list_for_each_entry(proto, &protocols, list) { if (proto->bind_all) { err |= proto->bind_all(proto, errmsg, errlen); @@ -57,6 +68,7 @@ int protocol_bind_all(char *errmsg, int errlen) break; } } + HA_SPIN_UNLOCK(PROTO_LOCK, &proto_lock); return err; } @@ -71,11 +83,13 @@ int protocol_unbind_all(void) int err; err = 0; + HA_SPIN_LOCK(PROTO_LOCK, &proto_lock); list_for_each_entry(proto, &protocols, list) { if (proto->unbind_all) { err |= proto->unbind_all(proto); } } + HA_SPIN_UNLOCK(PROTO_LOCK, &proto_lock); return err; } @@ -89,11 +103,13 @@ int protocol_enable_all(void) int err; err = 0; + HA_SPIN_LOCK(PROTO_LOCK, &proto_lock); list_for_each_entry(proto, &protocols, list) { if (proto->enable_all) { err |= proto->enable_all(proto); } } + HA_SPIN_UNLOCK(PROTO_LOCK, &proto_lock); return err; } @@ -107,11 +123,13 @@ int protocol_disable_all(void) int err; err = 0; + HA_SPIN_LOCK(PROTO_LOCK, &proto_lock); list_for_each_entry(proto, &protocols, list) { if (proto->disable_all) { err |= proto->disable_all(proto); } } + HA_SPIN_UNLOCK(PROTO_LOCK, &proto_lock); return err; }