From b7fea0f16d157bea291fb9c2e3f507bc95e45e8e Mon Sep 17 00:00:00 2001 From: Timo Sirainen Date: Wed, 30 Nov 2022 01:58:06 +0200 Subject: [PATCH] config, lib-master: Send configuration as a seekable file descriptor This allows lib-master to re-read the configuration using the same open fd. The binary configuration is written to an unlinked file. The file's fd is sent to config clients, so the binary config stays allocated until all config clients have closed the fd. --- src/config/config-connection.c | 32 ++++- src/config/config-dump-full.c | 86 +++++++++++-- src/config/config-dump-full.h | 9 +- src/config/config-request.c | 17 +++ src/config/config-request.h | 1 + src/config/doveconf.c | 36 ++---- src/lib-master/master-service-settings.c | 157 +++++++++++++---------- src/lib-master/master-service-settings.h | 1 + src/master/main.c | 1 + 9 files changed, 226 insertions(+), 114 deletions(-) diff --git a/src/config/config-connection.c b/src/config/config-connection.c index 9a7fa68ee3..e2225b6b0d 100644 --- a/src/config/config-connection.c +++ b/src/config/config-connection.c @@ -5,6 +5,7 @@ #include "llist.h" #include "istream.h" #include "ostream.h" +#include "ostream-unix.h" #include "strescape.h" #include "settings-parser.h" #include "master-service.h" @@ -34,6 +35,7 @@ struct config_connection { }; static struct config_connection *config_connections = NULL; +static int global_config_fd = -1; static const char *const * config_connection_next_line(struct config_connection *conn) @@ -52,10 +54,31 @@ static int config_connection_request(struct config_connection *conn, { const char *import_environment; - if (config_dump_full(conn->output, &import_environment) < 0) { - config_connection_destroy(conn); - return -1; + if (null_strcmp(*args, "reload") == 0) { + const char *path, *error; + + path = master_service_get_config_path(master_service); + if (config_parse_file(path, TRUE, NULL, &error) <= 0) { + o_stream_nsend_str(conn->output, + t_strconcat("-", error, "\n", NULL)); + return 0; + } + i_close_fd(&global_config_fd); + } + + if (global_config_fd == -1) { + int fd = config_dump_full(CONFIG_DUMP_FULL_DEST_RUNDIR, + &import_environment); + if (fd == -1) { + o_stream_nsend_str(conn->output, "-Failed\n"); + return 0; + } + global_config_fd = fd; } + if (!o_stream_unix_write_fd(conn->output, global_config_fd)) + i_unreached(); + + o_stream_nsend_str(conn->output, "+\n"); return 0; } @@ -105,7 +128,7 @@ struct config_connection *config_connection_create(int fd) conn = i_new(struct config_connection, 1); conn->fd = fd; conn->input = i_stream_create_fd(fd, MAX_INBUF_SIZE); - conn->output = o_stream_create_fd(fd, SIZE_MAX); + conn->output = o_stream_create_unix(fd, SIZE_MAX); o_stream_set_no_error_handling(conn->output, TRUE); conn->io = io_add(fd, IO_READ, config_connection_input, conn); DLLIST_PREPEND(&config_connections, conn); @@ -130,4 +153,5 @@ void config_connections_destroy_all(void) { while (config_connections != NULL) config_connection_destroy(config_connections); + i_close_fd(&global_config_fd); } diff --git a/src/config/config-dump-full.c b/src/config/config-dump-full.c index af305c8050..aef944b8c6 100644 --- a/src/config/config-dump-full.c +++ b/src/config/config-dump-full.c @@ -3,6 +3,7 @@ #include "lib.h" #include "str.h" #include "strescape.h" +#include "safe-mkstemp.h" #include "ostream.h" #include "config-parser.h" #include "config-request.h" @@ -70,7 +71,7 @@ config_dump_full_write_filter(struct ostream *output, o_stream_nsend(output, str_data(str), str_len(str)); } -static int +static bool config_dump_full_sections(struct ostream *output, unsigned int section_idx) { struct config_filter_parser *const *filters; @@ -98,22 +99,20 @@ config_dump_full_sections(struct ostream *output, unsigned int section_idx) config_export_parsers(export_ctx, (*filters)->parsers); ret = config_export_finish(&export_ctx, §ion_idx); } T_END; - return 0; + return ret == 0; } -int config_dump_full(struct ostream *output, const char **import_environment_r) +int config_dump_full(enum config_dump_full_dest dest, + const char **import_environment_r) { struct config_export_context *export_ctx; struct config_filter empty_filter; enum config_dump_flags flags; unsigned int section_idx = 0; - int ret; - - i_zero(&empty_filter); - o_stream_cork(output); + int fd = -1; + bool failed = FALSE; struct dump_context dump_ctx = { - .output = output, .delayed_output = str_new(default_pool, 256), }; @@ -123,12 +122,51 @@ int config_dump_full(struct ostream *output, const char **import_environment_r) i_zero(&empty_filter); config_export_by_filter(export_ctx, &empty_filter); + string_t *path = t_str_new(128); + const char *final_path = NULL; + switch (dest) { + case CONFIG_DUMP_FULL_DEST_RUNDIR: { + const char *base_dir = config_export_get_base_dir(export_ctx); + final_path = t_strdup_printf("%s/dovecot.conf.binary", base_dir); + str_append(path, final_path); + str_append_c(path, '.'); + break; + } + case CONFIG_DUMP_FULL_DEST_TEMPDIR: + /* create an unlinked file to /tmp */ + str_append(path, "/tmp/doveconf."); + break; + case CONFIG_DUMP_FULL_DEST_STDOUT: + dump_ctx.output = o_stream_create_fd(STDOUT_FILENO, IO_BLOCK_SIZE); + o_stream_set_name(dump_ctx.output, ""); + fd = 0; + break; + } + + if (dump_ctx.output == NULL) { + fd = safe_mkstemp(path, 0700, (uid_t)-1, (gid_t)-1); + if (fd == -1) { + i_error("safe_mkstemp(%s) failed: %m", str_c(path)); + (void)config_export_finish(&export_ctx, §ion_idx); + str_free(&dump_ctx.delayed_output); + return -1; + } + if (dest == CONFIG_DUMP_FULL_DEST_TEMPDIR) + i_unlink(str_c(path)); + dump_ctx.output = o_stream_create_fd(fd, IO_BLOCK_SIZE); + o_stream_set_name(dump_ctx.output, str_c(path)); + } + struct ostream *output = dump_ctx.output; + + i_zero(&empty_filter); + o_stream_cork(output); + *import_environment_r = t_strdup(config_export_get_import_environment(export_ctx)); if (config_export_finish(&export_ctx, §ion_idx) < 0) - ret = -1; + failed = TRUE; else - ret = config_dump_full_sections(output, section_idx); + failed = !config_dump_full_sections(output, section_idx); if (dump_ctx.delayed_output != NULL && str_len(dump_ctx.delayed_output) > 0) { @@ -139,6 +177,30 @@ int config_dump_full(struct ostream *output, const char **import_environment_r) str_free(&dump_ctx.delayed_output); o_stream_nsend_str(output, "\n"); - o_stream_uncork(output); - return ret; + if (o_stream_finish(output) < 0) { + i_error("write(%s) failed: %s", + o_stream_get_name(output), o_stream_get_error(output)); + failed = TRUE; + } + + if (final_path != NULL && !failed) { + if (rename(str_c(path), final_path) < 0) { + i_error("rename(%s, %s) failed: %m", + str_c(path), final_path); + /* the fd is still readable, so don't return failure */ + } + } + if (!failed && dest != CONFIG_DUMP_FULL_DEST_STDOUT && + lseek(fd, 0, SEEK_SET) < 0) { + i_error("lseek(%s, 0) failed: %m", o_stream_get_name(output)); + failed = TRUE; + } + if (failed) { + if (dest == CONFIG_DUMP_FULL_DEST_STDOUT) + fd = -1; + else + i_close_fd(&fd); + } + o_stream_destroy(&output); + return fd; } diff --git a/src/config/config-dump-full.h b/src/config/config-dump-full.h index b10ef8bdeb..56a364a3e3 100644 --- a/src/config/config-dump-full.h +++ b/src/config/config-dump-full.h @@ -1,6 +1,13 @@ #ifndef CONFIG_DUMP_FULL #define CONFIG_DUMP_FULL -int config_dump_full(struct ostream *output, const char **import_environment_r); +enum config_dump_full_dest { + CONFIG_DUMP_FULL_DEST_RUNDIR, + CONFIG_DUMP_FULL_DEST_TEMPDIR, + CONFIG_DUMP_FULL_DEST_STDOUT, +}; + +int config_dump_full(enum config_dump_full_dest dest, + const char **import_environment_r); #endif diff --git a/src/config/config-request.c b/src/config/config-request.c index 9e1019dd18..4aef57014f 100644 --- a/src/config/config-request.c +++ b/src/config/config-request.c @@ -442,6 +442,23 @@ config_export_get_import_environment(struct config_export_context *ctx) i_unreached(); } +const char *config_export_get_base_dir(struct config_export_context *ctx) +{ + enum setting_type stype; + unsigned int i; + + for (i = 0; ctx->parsers[i].root != NULL; i++) { + if (ctx->parsers[i].root == &master_service_setting_parser_info) { + const char *const *value = + settings_parse_get_value(ctx->parsers[i].parser, + "base_dir", &stype); + i_assert(value != NULL); + return *value; + } + } + i_unreached(); +} + static void config_export_free(struct config_export_context *ctx) { if (ctx->dup_parsers != NULL) diff --git a/src/config/config-request.h b/src/config/config-request.h index cda6fd9073..a7fac72dae 100644 --- a/src/config/config-request.h +++ b/src/config/config-request.h @@ -52,6 +52,7 @@ void config_export_get_output(struct config_export_context *ctx, struct master_service_settings_output *output_r); const char * config_export_get_import_environment(struct config_export_context *ctx); +const char *config_export_get_base_dir(struct config_export_context *ctx); int config_export_finish(struct config_export_context **ctx, unsigned int *section_idx); diff --git a/src/config/doveconf.c b/src/config/doveconf.c index 3a3d3be596..dd75b8f251 100644 --- a/src/config/doveconf.c +++ b/src/config/doveconf.c @@ -11,7 +11,6 @@ #include "ostream.h" #include "str.h" #include "strescape.h" -#include "safe-mkstemp.h" #include "settings-parser.h" #include "master-interface.h" #include "master-service.h" @@ -24,7 +23,6 @@ #include "config-request.h" #include "dovecot-version.h" -#include #include #include #include @@ -979,28 +977,11 @@ int main(int argc, char *argv[]) i_fatal("%s", error); if (dump_full && exec_args == NULL) { - struct ostream *output = o_stream_create_fd(STDOUT_FILENO, 0); - o_stream_set_no_error_handling(output, TRUE); - ret2 = config_dump_full(output, &import_environment); - o_stream_destroy(&output); + ret2 = config_dump_full(CONFIG_DUMP_FULL_DEST_STDOUT, + &import_environment); } else if (dump_full) { - /* create an unlinked file to /tmp */ - string_t *path = t_str_new(128); - str_append(path, "/tmp/doveconf."); - int temp_fd = safe_mkstemp(path, 0700, (uid_t)-1, (gid_t)-1); - if (temp_fd == -1) - i_fatal("safe_mkstemp(%s) failed: %m", str_c(path)); - i_unlink(str_c(path)); - struct ostream *output = - o_stream_create_fd(temp_fd, IO_BLOCK_SIZE); - if (config_dump_full(output, &import_environment) < 0) - i_fatal("Invalid configuration"); - if (o_stream_finish(output) < 0) { - i_fatal("write(%s) failed: %s", - str_c(path), o_stream_get_error(output)); - } - o_stream_destroy(&output); - + int temp_fd = config_dump_full(CONFIG_DUMP_FULL_DEST_TEMPDIR, + &import_environment); if (getenv(DOVECOT_PRESERVE_ENVS_ENV) != NULL) { /* Standalone binary is getting its configuration via doveconf. Clean the environment before calling it. @@ -1010,9 +991,12 @@ int main(int argc, char *argv[]) master_service_import_environment(import_environment); master_service_env_clean(); } - env_put(DOVECOT_CONFIG_FD_ENV, dec2str(temp_fd)); - execvp(exec_args[0], exec_args); - i_fatal("execvp(%s) failed: %m", exec_args[0]); + if (temp_fd != -1) { + env_put(DOVECOT_CONFIG_FD_ENV, dec2str(temp_fd)); + execvp(exec_args[0], exec_args); + i_fatal("execvp(%s) failed: %m", exec_args[0]); + } + ret2 = -1; } else if (simple_output) { struct config_export_context *ctx; unsigned int section_idx = 0; diff --git a/src/lib-master/master-service-settings.c b/src/lib-master/master-service-settings.c index b7357ab480..42775fe29c 100644 --- a/src/lib-master/master-service-settings.c +++ b/src/lib-master/master-service-settings.c @@ -5,6 +5,7 @@ #include "event-filter.h" #include "path-util.h" #include "istream.h" +#include "fdpass.h" #include "write-full.h" #include "str.h" #include "strescape.h" @@ -275,7 +276,7 @@ master_service_open_config(struct master_service *service, { struct stat st; const char *path; - int fd; + int fd = -1; *path_r = path = input->config_path != NULL ? input->config_path : master_service_get_config_path(service); @@ -297,52 +298,75 @@ master_service_open_config(struct master_service *service, this fails because the socket is 0600. it's useful for developers though. :) */ fd = net_connect_unix(DOVECOT_CONFIG_SOCKET_PATH); - if (fd >= 0) { + if (fd >= 0) *path_r = DOVECOT_CONFIG_SOCKET_PATH; - net_set_nonblock(fd, FALSE); - return fd; + else { + /* fallback to executing doveconf */ } - /* fallback to executing doveconf */ } - if (stat(path, &st) < 0) { - *error_r = errno == EACCES ? eacces_error_get("stat", path) : - t_strdup_printf("stat(%s) failed: %m", path); - config_error_update_path_source(service, input, error_r); - return -1; - } + if (fd == -1) { + if (stat(path, &st) < 0) { + *error_r = errno == EACCES ? + eacces_error_get("stat", path) : + t_strdup_printf("stat(%s) failed: %m", path); + config_error_update_path_source(service, input, error_r); + return -1; + } - if (!S_ISSOCK(st.st_mode) && !S_ISFIFO(st.st_mode)) { - /* it's not an UNIX socket, don't even try to connect */ - fd = -1; - errno = ENOTSOCK; - } else { - fd = net_connect_unix_with_retries(path, 1000); - } - if (fd < 0) { - *error_r = t_strdup_printf("net_connect_unix(%s) failed: %m", - path); - config_exec_fallback(service, input, error_r); - return -1; + if (!S_ISSOCK(st.st_mode) && !S_ISFIFO(st.st_mode)) { + /* it's not an UNIX socket, don't even try to connect */ + fd = -1; + errno = ENOTSOCK; + } else { + fd = net_connect_unix_with_retries(path, 1000); + } + if (fd < 0) { + *error_r = t_strdup_printf( + "net_connect_unix(%s) failed: %m", path); + config_exec_fallback(service, input, error_r); + return -1; + } } net_set_nonblock(fd, FALSE); - return fd; -} + const char *str = !input->reload_config ? + CONFIG_HANDSHAKE"REQ\n" : + CONFIG_HANDSHAKE"REQ\treload\n"; + alarm(CONFIG_READ_TIMEOUT_SECS); + int ret = write_full(fd, str, strlen(str)); + if (ret < 0) + *error_r = t_strdup_printf("write_full(%s) failed: %m", path); -static int -config_send_request(int fd, const char *path, const char **error_r) -{ - int ret; + int config_fd = -1; + if (ret == 0) { + /* read the config fd as reply */ + char buf[1024]; + ret = fd_read(fd, buf, sizeof(buf)-1, &config_fd); + if (ret < 0) + *error_r = t_strdup_printf("fd_read() failed: %m"); + else if (ret > 0 && buf[0] == '+' && buf[1] == '\n') + ; /* success */ + else if (ret > 0 && buf[0] == '-') { + buf[ret] = '\0'; + *error_r = t_strdup_printf("Failed to read config: %s", + t_strcut(buf+1, '\n')); + i_close_fd(&config_fd); + } else { + buf[ret] = '\0'; + *error_r = t_strdup_printf( + "Failed to read config: Unexpected reply '%s'", + t_strcut(buf, '\n')); + i_close_fd(&config_fd); + } + } + alarm(0); + i_close_fd(&fd); - T_BEGIN { - const char *str = CONFIG_HANDSHAKE"REQ\n"; - ret = write_full(fd, str, strlen(str)); - } T_END; - if (ret < 0) { - *error_r = t_strdup_printf("write_full(%s) failed: %m", path); + if (config_fd == -1) { + config_exec_fallback(service, input, error_r); return -1; } - return 0; + return config_fd; } static int @@ -387,8 +411,10 @@ void master_service_config_socket_try_open(struct master_service *service) i_zero(&input); input.never_exec = TRUE; fd = master_service_open_config(service, &input, &path, &error); - if (fd != -1) + if (fd != -1) { service->config_fd = fd; + fd_close_on_exec(service->config_fd, TRUE); + } } static void @@ -434,10 +460,6 @@ master_service_settings_read_stream(struct setting_parser_context *parser, return 0; } linenum++; - if (linenum == 1 && str_begins(line, "ERROR ", &line)) { - *error_r = t_strdup(line); - return -1; - } if (str_begins(line, ":FILTER ", &filter_string)) { struct event_filter *filter = event_filter_create(); filter_string_parse_protocol(filter_string, &protocols); @@ -478,11 +500,11 @@ master_service_settings_read_stream(struct setting_parser_context *parser, i_stream_get_error(input)); } else if (input->v_offset == 0) { *error_r = t_strdup_printf( - "read(%s) disconnected before receiving any data", + "read(%s) returned EOF before receiving any data", i_stream_get_name(input)); } else { *error_r = t_strdup_printf( - "read(%s) disconnected before receiving end-of-settings line", + "read(%s) returned EOF before receiving end-of-settings line", i_stream_get_name(input)); } return -1; @@ -500,41 +522,33 @@ int master_service_settings_read(struct master_service *service, const char *path = NULL, *value, *error; unsigned int i; int ret, fd = -1; - bool use_environment = FALSE, retry; + bool use_environment = FALSE; i_zero(output_r); - value = getenv(DOVECOT_CONFIG_FD_ENV); - if (value != NULL) { + if (service->config_fd != -1 && !input->reload_config) { + /* config was already read once */ + fd = service->config_fd; + if (lseek(fd, 0, SEEK_SET) < 0) + i_fatal("lseek(config fd) failed: %m"); + path = ""; + } else if ((value = getenv(DOVECOT_CONFIG_FD_ENV)) != NULL) { /* doveconf -F parameter already executed us back. The configuration is in DOVECOT_CONFIG_FD. */ if (str_to_int(value, &fd) < 0 || fd < 0) i_fatal("Invalid "DOVECOT_CONFIG_FD_ENV": %s", value); + path = t_strdup_printf("<"DOVECOT_CONFIG_FD_ENV" %d>", fd); } else if (getenv("DOVECONF_ENV") != NULL) { use_environment = (service->flags & MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS) == 0; } else if ((service->flags & MASTER_SERVICE_FLAG_NO_CONFIG_SETTINGS) == 0) { /* Open config via socket if possible. If it doesn't work, execute doveconf -F. */ - retry = service->config_fd != -1; - for (;;) { - fd = master_service_open_config(service, input, - &path, error_r); - if (fd == -1) { - if (errno == EACCES) - output_r->permission_denied = TRUE; - return -1; - } - - if (config_send_request(fd, path, error_r) == 0) - break; - i_close_fd(&fd); - if (!retry) { - config_exec_fallback(service, input, error_r); - return -1; - } - /* config process died, retry connecting */ - retry = FALSE; + fd = master_service_open_config(service, input, &path, error_r); + if (fd == -1) { + if (errno == EACCES) + output_r->permission_denied = TRUE; + return -1; } } @@ -575,10 +589,9 @@ int master_service_settings_read(struct master_service *service, if (fd != -1) { istream = i_stream_create_fd(fd, SIZE_MAX); - alarm(CONFIG_READ_TIMEOUT_SECS); + i_stream_set_name(istream, path); ret = master_service_settings_read_stream(parser, event, istream, output_r, error_r); - alarm(0); i_stream_unref(&istream); if (ret < 0) { @@ -593,12 +606,14 @@ int master_service_settings_read(struct master_service *service, return -1; } - if ((service->flags & MASTER_SERVICE_FLAG_KEEP_CONFIG_OPEN) != 0 && - service->config_fd == -1 && input->config_path == NULL && - getenv(DOVECOT_CONFIG_FD_ENV) == NULL) + if (input->config_path == NULL) { + i_assert(service->config_fd == -1 || + service->config_fd == fd); service->config_fd = fd; - else + fd_close_on_exec(service->config_fd, TRUE); + } else { i_close_fd(&fd); + } env_remove(DOVECOT_CONFIG_FD_ENV); } event_unref(&event); diff --git a/src/lib-master/master-service-settings.h b/src/lib-master/master-service-settings.h index 846085d330..bfde3ab9c7 100644 --- a/src/lib-master/master-service-settings.h +++ b/src/lib-master/master-service-settings.h @@ -38,6 +38,7 @@ struct master_service_settings_input { bool preserve_environment; bool preserve_user; bool preserve_home; + bool reload_config; bool never_exec; bool always_exec; bool use_sysexits; diff --git a/src/master/main.c b/src/master/main.c index 116bce0dc2..afc0b000fa 100644 --- a/src/master/main.c +++ b/src/master/main.c @@ -419,6 +419,7 @@ sig_settings_reload(const siginfo_t *si ATTR_UNUSED, input.roots = set_roots; input.config_path = services_get_config_socket_path(services); input.never_exec = TRUE; + input.reload_config = TRUE; if (master_service_settings_read(master_service, &input, &output, &error) < 0) { i_error("Error reading configuration: %s", error); -- 2.47.3