From ghudson at mit.edu Mon Feb 3 18:55:42 2025 From: ghudson at mit.edu (ghudson at mit.edu) Date: Mon, 3 Feb 2025 18:55:42 -0500 (EST) Subject: krb5 commit: Add support for systemd socket activation Message-ID: <20250203235542.718BC101A79@krbdev.mit.edu> https://github.com/krb5/krb5/commit/11b9a8244f1b3f5226f315e998f4e892b262e46e commit 11b9a8244f1b3f5226f315e998f4e892b262e46e Author: Andreas Schneider Date: Wed Nov 20 18:17:29 2024 +0100 Add support for systemd socket activation If LISTEN_PID and LISTEN_FDS are set in the environment, expect listener sockets to be present at fds starting at 3. If any match a configured listener address and port, use it instead of creating a new socket. [ghudson at mit.edu: combined two commits; changed sa_compare() to sa_equal(); restructured changes to fetch listener sockets into a list once and to match on socket type as well as address; added tests] ticket: 9157 (new) .gitignore | 1 + doc/admin/admin_commands/kadmind.rst | 8 ++ doc/admin/admin_commands/krb5kdc.rst | 7 ++ src/include/socket-utils.h | 35 +++++++++ src/kdc/Makefile.in | 10 ++- src/kdc/deps | 10 +++ src/kdc/t_sockact.c | 121 +++++++++++++++++++++++++++++ src/kdc/t_sockact.py | 43 +++++++++++ src/lib/apputils/net-server.c | 144 ++++++++++++++++++++++++++++++----- 9 files changed, 358 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index a7a217a6f..ae6780c37 100644 --- a/.gitignore +++ b/.gitignore @@ -258,6 +258,7 @@ local.properties /src/kdc/rtest /src/kdc/t_ndr /src/kdc/t_replay +/src/kdc/t_sockact /src/lib/k5sprt32.def diff --git a/doc/admin/admin_commands/kadmind.rst b/doc/admin/admin_commands/kadmind.rst index 7e1482635..bc66890de 100644 --- a/doc/admin/admin_commands/kadmind.rst +++ b/doc/admin/admin_commands/kadmind.rst @@ -121,6 +121,14 @@ ENVIRONMENT See :ref:`kerberos(7)` for a description of Kerberos environment variables. +As of release 1.22, kadmind supports systemd socket activation via the +LISTEN_PID and LISTEN_FDS environment variables. Sockets provided by +the caller must correspond to configured listener addresses (via the +**kadmind_listen** or **kpasswd_listen** variables or equivalents) or +they will be ignored. Any configured listener addresses that do not +correspond to caller-provided sockets will be ignored if socket +activation is used. + SEE ALSO -------- diff --git a/doc/admin/admin_commands/krb5kdc.rst b/doc/admin/admin_commands/krb5kdc.rst index 631a0de84..97fbe5ed7 100644 --- a/doc/admin/admin_commands/krb5kdc.rst +++ b/doc/admin/admin_commands/krb5kdc.rst @@ -106,6 +106,13 @@ ENVIRONMENT See :ref:`kerberos(7)` for a description of Kerberos environment variables. +As of release 1.22, krb5kdc supports systemd socket activation via the +LISTEN_PID and LISTEN_FDS environment variables. Sockets provided by +the caller must correspond to configured listener addresses (via the +**kdc_listen** variable or equivalent) or they will be ignored. Any +configured listener addresses that do not correspond to +caller-provided sockets will be ignored if socket activation is used. + SEE ALSO -------- diff --git a/src/include/socket-utils.h b/src/include/socket-utils.h index 177662c87..02c10ec02 100644 --- a/src/include/socket-utils.h +++ b/src/include/socket-utils.h @@ -43,6 +43,8 @@ #ifndef SOCKET_UTILS_H #define SOCKET_UTILS_H +#include + /* Some useful stuff cross-platform for manipulating socket addresses. We assume at least ipv4 sockaddr_in support. The sockaddr_storage stuff comes from the ipv6 socket api enhancements; socklen_t is @@ -159,4 +161,37 @@ sa_socklen(const struct sockaddr *sa) abort(); } +/* Return true if a and b are the same address (and port if applicable). */ +static inline bool +sa_equal(const struct sockaddr *a, const struct sockaddr *b) +{ + if (a == NULL || b == NULL || a->sa_family != b->sa_family) + return false; + + if (a->sa_family == AF_INET) { + const struct sockaddr_in *x = sa2sin(a); + const struct sockaddr_in *y = sa2sin(b); + + if (x->sin_port != y->sin_port) + return false; + return memcmp(&x->sin_addr, &y->sin_addr, sizeof(x->sin_addr)) == 0; + } else if (a->sa_family == AF_INET6) { + const struct sockaddr_in6 *x = sa2sin6(a); + const struct sockaddr_in6 *y = sa2sin6(b); + + if (x->sin6_port != y->sin6_port) + return false; + return memcmp(&x->sin6_addr, &y->sin6_addr, sizeof(x->sin6_addr)) == 0; +#ifndef _WIN32 + } else if (a->sa_family == AF_UNIX) { + const struct sockaddr_un *x = sa2sun(a); + const struct sockaddr_un *y = sa2sun(b); + + return strcmp(x->sun_path, y->sun_path) == 0; +#endif + } + + return false; +} + #endif /* SOCKET_UTILS_H */ diff --git a/src/kdc/Makefile.in b/src/kdc/Makefile.in index 7199b3472..bf4d9b580 100644 --- a/src/kdc/Makefile.in +++ b/src/kdc/Makefile.in @@ -31,7 +31,8 @@ SRCS= \ EXTRADEPSRCS= \ $(srcdir)/t_ndr.c \ - $(srcdir)/t_replay.c + $(srcdir)/t_replay.c \ + $(srcdir)/t_sockact.c OBJS= \ authind.o \ @@ -78,16 +79,21 @@ T_REPLAY_OBJS=t_replay.o t_replay: $(T_REPLAY_OBJS) replay.o $(KRB5_BASE_DEPLIBS) $(CC_LINK) -o $@ $(T_REPLAY_OBJS) $(CMOCKA_LIBS) $(KRB5_BASE_LIBS) +t_sockact: t_sockact.o $(KRB5_BASE_DEPLIBS) + $(CC_LINK) -o $@ t_sockact.o $(KRB5_BASE_LIBS) + check-cmocka: t_replay $(RUN_TEST) ./t_replay > /dev/null -check-pytests: +check-pytests: t_sockact $(RUNPYTEST) $(srcdir)/t_workers.py $(PYTESTFLAGS) $(RUNPYTEST) $(srcdir)/t_emptytgt.py $(PYTESTFLAGS) $(RUNPYTEST) $(srcdir)/t_bigreply.py $(PYTESTFLAGS) + $(RUNPYTEST) $(srcdir)/t_sockact.py $(PYTESTFLAGS) install: $(INSTALL_PROGRAM) krb5kdc ${DESTDIR}$(SERVER_BINDIR)/krb5kdc clean: $(RM) krb5kdc rtest.o rtest t_replay.o t_replay t_ndr.o t_ndr + $(RM) t_sockact.o t_sockact diff --git a/src/kdc/deps b/src/kdc/deps index 2d54fa93d..86be4c475 100644 --- a/src/kdc/deps +++ b/src/kdc/deps @@ -427,3 +427,13 @@ $(OUTPRE)t_replay.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ $(top_srcdir)/include/port-sockets.h $(top_srcdir)/include/socket-utils.h \ extern.h kdc_util.h realm_data.h replay.c reqstate.h \ t_replay.c +$(OUTPRE)t_sockact.$(OBJEXT): $(BUILDTOP)/include/autoconf.h \ + $(BUILDTOP)/include/krb5/krb5.h $(BUILDTOP)/include/osconf.h \ + $(BUILDTOP)/include/profile.h $(COM_ERR_DEPS) $(top_srcdir)/include/k5-buf.h \ + $(top_srcdir)/include/k5-err.h $(top_srcdir)/include/k5-gmt_mktime.h \ + $(top_srcdir)/include/k5-int-pkinit.h $(top_srcdir)/include/k5-int.h \ + $(top_srcdir)/include/k5-platform.h $(top_srcdir)/include/k5-plugin.h \ + $(top_srcdir)/include/k5-thread.h $(top_srcdir)/include/k5-trace.h \ + $(top_srcdir)/include/krb5.h $(top_srcdir)/include/krb5/authdata_plugin.h \ + $(top_srcdir)/include/krb5/plugin.h $(top_srcdir)/include/port-sockets.h \ + $(top_srcdir)/include/socket-utils.h t_sockact.c diff --git a/src/kdc/t_sockact.c b/src/kdc/t_sockact.c new file mode 100644 index 000000000..cb4a4bc7a --- /dev/null +++ b/src/kdc/t_sockact.c @@ -0,0 +1,121 @@ +/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* kdc/t_sockact.c - socket activation test harness */ +/* + * Copyright (C) 2025 by the Massachusetts Institute of Technology. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Usage: t_sockact address... -- program args... + * + * This program simulates systemd socket activation by creating one or more + * listener sockets at the specified addresses, setting LISTEN_FDS and + * LISTEN_PID in the environment, and executing the specified command. (The + * real systemd would not execute the program until there is input on one of + * the listener sockets, but we do not need to simulate that, and executing the + * command immediately allow easier integration with k5test.py.) + */ + +#include "k5-int.h" +#include "socket-utils.h" + +static int max_fd; + +static void +create_socket(const struct sockaddr *addr) +{ + int fd, one = 1; + + fd = socket(addr->sa_family, SOCK_STREAM, 0); + if (fd < 0) + abort(); + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)) != 0) + abort(); +#ifdef SO_REUSEPORT + if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one)) != 0) + abort(); +#endif +#if defined(IPV6_V6ONLY) + if (addr->sa_family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one)) != 0) + abort(); + } +#endif + if (bind(fd, addr, sa_socklen(addr)) != 0) + abort(); + if (listen(fd, 5) != 0) + abort(); + max_fd = fd; +} + +int +main(int argc, char **argv) +{ + const char *addrstr; + struct sockaddr_storage ss = { 0 }; + struct addrinfo hints = { 0 }, *ai_list = NULL, *ai = NULL; + char *host, nbuf[128]; + int i, port; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--") == 0) + break; + addrstr = argv[i]; + if (*addrstr == '/') { + ss.ss_family = AF_UNIX; + strlcpy(ss2sun(&ss)->sun_path, addrstr, + sizeof(ss2sun(&ss)->sun_path)); + create_socket(ss2sa(&ss)); + } else { + if (k5_parse_host_string(addrstr, 0, &host, &port) != 0 || !port) + abort(); + hints.ai_socktype = SOCK_STREAM; + hints.ai_family = AF_UNSPEC; + hints.ai_flags = AI_PASSIVE; +#ifdef AI_NUMERICSERV + hints.ai_flags |= AI_NUMERICSERV; +#endif + (void)snprintf(nbuf, sizeof(nbuf), "%d", port); + if (getaddrinfo(host, nbuf, &hints, &ai_list) != 0) + abort(); + for (ai = ai_list; ai != NULL; ai = ai->ai_next) + create_socket(ai->ai_addr); + freeaddrinfo(ai_list); + free(host); + } + } + argv += i + 1; + + (void)snprintf(nbuf, sizeof(nbuf), "%d", max_fd - 2); + setenv("LISTEN_FDS", nbuf, 1); + (void)snprintf(nbuf, sizeof(nbuf), "%lu", (unsigned long)getpid()); + setenv("LISTEN_PID", nbuf, 1); + execv(argv[0], argv); + abort(); + return 1; +} diff --git a/src/kdc/t_sockact.py b/src/kdc/t_sockact.py new file mode 100644 index 000000000..2a4aeb9ed --- /dev/null +++ b/src/kdc/t_sockact.py @@ -0,0 +1,43 @@ +from k5test import * + +if not which('systemd-socket-activate'): + skip_rest('socket activation tests', 'systemd-socket-activate not found') + +# Configure listeners for two UNIX domain sockets and two ports. +kdc_conf = {'realms': {'$realm': { + 'kdc_listen': '$testdir/sock1 $testdir/sock2', + 'kdc_tcp_listen': '$port7 $port8'}}} +realm = K5Realm(kdc_conf=kdc_conf, start_kdc=False) + +# Create socket activation fds for just one of the UNIX domain sockets +# and one of the ports. +realm.start_server(['./t_sockact', os.path.join(realm.testdir, 'sock1'), + str(realm.portbase + 8), '--', krb5kdc, '-n'], + 'starting...') + +mark('UNIX socket 1') +cconf1 = {'realms': {'$realm': {'kdc': '$testdir/sock1'}}} +env1 = realm.special_env('sock1', False, krb5_conf=cconf1) +realm.kinit(realm.user_princ, password('user'), env=env1) + +mark('port8') +cconf2 = {'realms': {'$realm': {'kdc': '$hostname:$port8'}}} +env2 = realm.special_env('sock1', False, krb5_conf=cconf2) +realm.kinit(realm.user_princ, password('user'), env=env2) + +# Test that configured listener addresses are ignored if they don't +# match caller-provided sockets. + +mark('UNIX socket 2') +cconf3 = {'realms': {'$realm': {'kdc': '$testdir/sock2'}}} +env3 = realm.special_env('sock2', False, krb5_conf=cconf3) +realm.kinit(realm.user_princ, password('user'), env=env3, expected_code=1, + expected_msg='Cannot contact any KDC') + +mark('port7') +cconf4 = {'realms': {'$realm': {'kdc': '$hostname:$port7'}}} +env4 = realm.special_env('sock1', False, krb5_conf=cconf4) +realm.kinit(realm.user_princ, password('user'), env=env3, expected_code=1, + expected_msg='Cannot contact any KDC') + +success('systemd socket activation tests') diff --git a/src/lib/apputils/net-server.c b/src/lib/apputils/net-server.c index 6fa8a97e0..c8c83606d 100644 --- a/src/lib/apputils/net-server.c +++ b/src/lib/apputils/net-server.c @@ -65,6 +65,19 @@ #include "udppktinfo.h" +/* List of systemd socket activation addresses and socket types. */ +struct sockact_list { + size_t nsockets; + struct { + struct sockaddr_storage addr; + int type; + } *fds; +}; + +/* When systemd socket activation is used, caller-provided sockets begin at + * file descriptor 3. */ +const int SOCKACT_START = 3; + /* XXX */ #define KDC5_NONET (-1779992062L) @@ -694,6 +707,82 @@ static const enum conn_type bind_conn_types[] = [UNX] = CONN_UNIXSOCK_LISTENER }; +/* If any systemd socket activation fds are indicated by the environment, set + * them close-on-exec and put their addresses and socket types into *list. */ +static void +init_sockact_list(struct sockact_list *list) +{ + const char *v; + char *end; + long lpid; + int fd; + size_t nfds, i; + socklen_t slen; + + list->nsockets = 0; + list->fds = NULL; + + /* Check if LISTEN_FDS is meant for this process. */ + v = getenv("LISTEN_PID"); + if (v == NULL) + return; + lpid = strtol(v, &end, 10); + if (end == NULL || end == v || *end != '\0' || lpid != getpid()) + return; + + /* Get the number of activated sockets. */ + v = getenv("LISTEN_FDS"); + if (v == NULL) + return; + nfds = strtoul(v, &end, 10); + if (end == NULL || end == v || *end != '\0') + return; + if (nfds == 0 || nfds > (size_t)INT_MAX - SOCKACT_START) + return; + + list->fds = calloc(nfds, sizeof(*list->fds)); + if (list->fds == NULL) + return; + + for (i = 0; i < nfds; i++) { + fd = i + SOCKACT_START; + set_cloexec_fd(fd); + slen = sizeof(list->fds[i].addr); + (void)getsockname(fd, ss2sa(&list->fds[i].addr), &slen); + slen = sizeof(list->fds[i].type); + (void)getsockopt(fd, SOL_SOCKET, SO_TYPE, &list->fds[i].type, &slen); + } + + list->nsockets = nfds; +} + +/* Release any storage used by *list. */ +static void +fini_sockact_list(struct sockact_list *list) +{ + free(list->fds); + list->fds = NULL; + list->nsockets = 0; +} + +/* If sa matches an address in *list, return the associated file descriptor and + * clear the address from *list. Otherwise return -1. */ +static int +find_sockact(struct sockact_list *list, const struct sockaddr *sa, int type) +{ + size_t i; + + for (i = 0; i < list->nsockets; i++) { + if (list->fds[i].type == type && + sa_equal(ss2sa(&list->fds[i].addr), sa)) { + list->fds[i].type = -1; + memset(&list->fds[i].addr, 0, sizeof(list->fds[i].addr)); + return i + SOCKACT_START; + } + } + return -1; +} + /* * Set up a listening socket. * @@ -708,8 +797,9 @@ static const enum conn_type bind_conn_types[] = */ static krb5_error_code setup_socket(struct bind_address *ba, struct sockaddr *sock_address, - void *handle, const char *prog, verto_ctx *ctx, - int listen_backlog, verto_callback vcb, enum conn_type ctype) + struct sockact_list *sockacts, void *handle, const char *prog, + verto_ctx *ctx, int listen_backlog, verto_callback vcb, + enum conn_type ctype) { krb5_error_code ret; struct connection *conn; @@ -722,20 +812,31 @@ setup_socket(struct bind_address *ba, struct sockaddr *sock_address, krb5_klog_syslog(LOG_DEBUG, _("Setting up %s socket for address %s"), bind_type_names[ba->type], addrbuf); - /* Create the socket. */ - ret = create_server_socket(sock_address, bind_socktypes[ba->type], prog, - &sock); - if (ret) - goto cleanup; + if (sockacts->nsockets > 0) { + /* Look for a systemd socket activation fd matching sock_address. */ + sock = find_sockact(sockacts, sock_address, bind_socktypes[ba->type]); + if (sock == -1) { + /* Ignore configured addresses that don't match any caller-provided + * sockets. */ + ret = 0; + goto cleanup; + } + } else { + /* We're not using socket activation; create the socket. */ + ret = create_server_socket(sock_address, bind_socktypes[ba->type], + prog, &sock); + if (ret) + goto cleanup; - /* Listen for backlogged connections on stream sockets. (For RPC sockets - * this will be done by svc_register().) */ - if ((ba->type == TCP || ba->type == UNX) && - listen(sock, listen_backlog) != 0) { - ret = errno; - com_err(prog, errno, _("Cannot listen on %s server socket on %s"), - bind_type_names[ba->type], addrbuf); - goto cleanup; + /* Listen for backlogged connections on stream sockets. (For RPC + * sockets this will be done by svc_register().) */ + if ((ba->type == TCP || ba->type == UNX) && + listen(sock, listen_backlog) != 0) { + ret = errno; + com_err(prog, errno, _("Cannot listen on %s server socket on %s"), + bind_type_names[ba->type], addrbuf); + goto cleanup; + } } /* Set non-blocking I/O for non-RPC listener sockets. */ @@ -837,6 +938,7 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, struct bind_address addr; struct sockaddr_un sun; struct addrinfo hints, *ai_list = NULL, *ai = NULL; + struct sockact_list sockacts = { 0 }; verto_callback vcb; char addrbuf[128]; @@ -855,6 +957,8 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, hints.ai_flags |= AI_NUMERICSERV; #endif + init_sockact_list(&sockacts); + /* Add all the requested addresses. */ for (i = 0; i < bind_addresses.n; i++) { addr = bind_addresses.data[i]; @@ -870,8 +974,9 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, addr.address); goto cleanup; } - ret = setup_socket(&addr, (struct sockaddr *)&sun, handle, prog, - ctx, listen_backlog, verto_callbacks[addr.type], + ret = setup_socket(&addr, (struct sockaddr *)&sun, &sockacts, + handle, prog, ctx, listen_backlog, + verto_callbacks[addr.type], bind_conn_types[addr.type]); if (ret) { krb5_klog_syslog(LOG_ERR, @@ -911,8 +1016,8 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, /* Set the real port number. */ sa_setport(ai->ai_addr, addr.port); - ret = setup_socket(&addr, ai->ai_addr, handle, prog, ctx, - listen_backlog, verto_callbacks[addr.type], + ret = setup_socket(&addr, ai->ai_addr, &sockacts, handle, prog, + ctx, listen_backlog, verto_callbacks[addr.type], bind_conn_types[addr.type]); if (ret) { k5_print_addr(ai->ai_addr, addrbuf, sizeof(addrbuf)); @@ -937,6 +1042,7 @@ setup_addresses(verto_ctx *ctx, void *handle, const char *prog, cleanup: if (ai_list != NULL) freeaddrinfo(ai_list); + fini_sockact_list(&sockacts); return ret; } From ghudson at mit.edu Thu Feb 6 18:02:18 2025 From: ghudson at mit.edu (ghudson at mit.edu) Date: Thu, 6 Feb 2025 18:02:18 -0500 (EST) Subject: krb5 commit: Allow only one salt type per enctype in key data Message-ID: <20250206230218.D1684101CA3@krbdev.mit.edu> https://github.com/krb5/krb5/commit/367ccd2fcf8b4adb82bfc7ef9b5f04ff94f80326 commit 367ccd2fcf8b4adb82bfc7ef9b5f04ff94f80326 Author: Greg Hudson Date: Wed Jan 29 00:22:57 2025 -0500 Allow only one salt type per enctype in key data In the default libkdb5 password change method, omit requested key/salt combinations that duplicate an earlier encryption type, even if they have a different salt type. Any use cases for multiple salts for the same enctype disappeared with single-DES support. (We already have this behavior for chrand requests.) ticket: 9160 (new) src/lib/kdb/kdb_cpw.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/kdb/kdb_cpw.c b/src/lib/kdb/kdb_cpw.c index c33c7cf8d..8b012e19e 100644 --- a/src/lib/kdb/kdb_cpw.c +++ b/src/lib/kdb/kdb_cpw.c @@ -264,8 +264,7 @@ add_key_pwd(krb5_context context, krb5_keyblock *master_key, &similar))) return(retval); - if (similar && - (ks_tuple[j].ks_salttype == ks_tuple[i].ks_salttype)) + if (similar) break; } From ghudson at mit.edu Thu Feb 6 18:43:34 2025 From: ghudson at mit.edu (ghudson at mit.edu) Date: Thu, 6 Feb 2025 18:43:34 -0500 (EST) Subject: krb5 commit: Improve ulog block resize efficiency Message-ID: <20250206234334.EA2D3101CA3@krbdev.mit.edu> https://github.com/krb5/krb5/commit/197ddd4196a3049cd17d29d1c8f0af1424472956 commit 197ddd4196a3049cd17d29d1c8f0af1424472956 Author: Zoltan Borbely Date: Fri Jan 31 18:00:17 2025 +0100 Improve ulog block resize efficiency When it is necessary to increase the ulog block size, copy the existing entries instead of reinitializing the log. [ghudson at mit.edu: added test case; renamed and split INDEX() to avoid duplication; added sync_ulog() helper; modified copying loop for clarity; edited commit message] ticket: 9161 (new) src/include/kdb_log.h | 20 ++++++++++----- src/kprop/kproplog.c | 2 +- src/lib/kdb/kdb_log.c | 68 +++++++++++++++++++++++++++++++++++---------------- src/tests/t_iprop.py | 30 +++++++++++++++++++++-- 4 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/include/kdb_log.h b/src/include/kdb_log.h index 423957565..2856859f8 100644 --- a/src/include/kdb_log.h +++ b/src/include/kdb_log.h @@ -18,12 +18,6 @@ extern "C" { #endif -/* - * DB macros - */ -#define INDEX(ulog, i) (kdb_ent_header_t *)(void *) \ - ((char *)(ulog) + sizeof(kdb_hlog_t) + (i) * ulog->kdb_block) - /* * Current DB version # */ @@ -106,6 +100,20 @@ typedef struct _kdb_log_context { int ulogfd; } kdb_log_context; +/* Return the address of the i'th record in ulog for the given block size. */ +static inline uint8_t * +ulog_record_ptr(kdb_hlog_t *ulog, size_t i, size_t bsize) +{ + return (uint8_t *)ulog + sizeof(*ulog) + i * bsize; +} + +/* Return the i'th update entry header for ulog. */ +static inline kdb_ent_header_t * +ulog_index(kdb_hlog_t *ulog, size_t i) +{ + return (void *)ulog_record_ptr(ulog, i, ulog->kdb_block); +} + #ifdef __cplusplus } #endif diff --git a/src/kprop/kproplog.c b/src/kprop/kproplog.c index 1f10aa6dc..45ded429e 100644 --- a/src/kprop/kproplog.c +++ b/src/kprop/kproplog.c @@ -330,7 +330,7 @@ print_update(kdb_hlog_t *ulog, uint32_t entry, uint32_t ulogentries, for (i = start_sno; i < ulog->kdb_last_sno; i++) { indx = i % ulogentries; - indx_log = INDEX(ulog, indx); + indx_log = ulog_index(ulog, indx); /* * Check for corrupt update entry diff --git a/src/lib/kdb/kdb_log.c b/src/lib/kdb/kdb_log.c index 68fae919a..b840eec9a 100644 --- a/src/lib/kdb/kdb_log.c +++ b/src/lib/kdb/kdb_log.c @@ -103,13 +103,31 @@ sync_header(kdb_hlog_t *ulog) } } +/* Sync memory to disk for the entire ulog. */ +static void +sync_ulog(kdb_hlog_t *ulog, uint32_t ulogentries) +{ + size_t len; + + if (!pagesize) + pagesize = getpagesize(); + + len = (sizeof(kdb_hlog_t) + ulogentries * ulog->kdb_block + + (pagesize - 1)) & ~(pagesize - 1); + if (msync(ulog, len, MS_SYNC)) { + /* Couldn't sync to disk, let's panic. */ + syslog(LOG_ERR, _("could not sync the whole ulog to disk")); + abort(); + } +} + /* Return true if the ulog entry for sno matches sno and timestamp. */ static krb5_boolean check_sno(kdb_log_context *log_ctx, kdb_sno_t sno, const kdbe_time_t *timestamp) { unsigned int indx = (sno - 1) % log_ctx->ulogentries; - kdb_ent_header_t *ent = INDEX(log_ctx->ulog, indx); + kdb_ent_header_t *ent = ulog_index(log_ctx->ulog, indx); return ent->kdb_entry_sno == sno && time_equal(&ent->kdb_time, timestamp); } @@ -175,17 +193,16 @@ extend_file_to(int fd, unsigned int new_size) return 0; } -/* - * Resize the array elements. We reinitialize the update log rather than - * unrolling the the log and copying it over to a temporary log for obvious - * performance reasons. Replicas will subsequently do a full resync, but the - * need for resizing should be very small. - */ +/* Resize the array elements of ulog to be at least as large as recsize. Move + * the existing elements into the proper offsets for the new block size. */ static krb5_error_code resize(kdb_hlog_t *ulog, uint32_t ulogentries, int ulogfd, unsigned int recsize, const kdb_incr_update_t *upd) { - unsigned int new_block, new_size; + size_t old_block = ulog->kdb_block, new_block, new_size; + krb5_error_code retval; + uint8_t *old_ent, *new_ent; + uint32_t i; if (ulog == NULL) return KRB5_LOG_ERROR; @@ -204,16 +221,25 @@ resize(kdb_hlog_t *ulog, uint32_t ulogentries, int ulogfd, if (new_size > MAXLOGLEN) return KRB5_LOG_ERROR; - /* Reinit log with new block size. */ - memset(ulog, 0, sizeof(*ulog)); - ulog->kdb_hmagic = KDB_ULOG_HDR_MAGIC; - ulog->db_version_num = KDB_VERSION; - ulog->kdb_state = KDB_STABLE; - ulog->kdb_block = new_block; - sync_header(ulog); - /* Expand log considering new block size. */ - return extend_file_to(ulogfd, new_size); + retval = extend_file_to(ulogfd, new_size); + if (retval) + return retval; + + /* Copy each record into its new location and zero out the unused areas. + * The area is overlapping, so we have to iterate backwards. */ + for (i = ulogentries; i > 0; i--) { + old_ent = ulog_record_ptr(ulog, i - 1, old_block); + new_ent = ulog_record_ptr(ulog, i - 1, new_block); + memmove(new_ent, old_ent, old_block); + memset(new_ent + old_block, 0, new_block - old_block); + } + + syslog(LOG_INFO, _("ulog block size has been resized from %lu to %lu"), + (unsigned long)old_block, (unsigned long)new_block); + ulog->kdb_block = new_block; + sync_ulog(ulog, ulogentries); + return 0; } /* Set the ulog to contain only a dummy entry with the given serial number and @@ -222,7 +248,7 @@ static void set_dummy(kdb_log_context *log_ctx, kdb_sno_t sno, const kdbe_time_t *kdb_time) { kdb_hlog_t *ulog = log_ctx->ulog; - kdb_ent_header_t *ent = INDEX(ulog, (sno - 1) % log_ctx->ulogentries); + kdb_ent_header_t *ent = ulog_index(ulog, (sno - 1) % log_ctx->ulogentries); memset(ent, 0, sizeof(*ent)); ent->kdb_umagic = KDB_ULOG_MAGIC; @@ -305,7 +331,7 @@ store_update(kdb_log_context *log_ctx, kdb_incr_update_t *upd) ulog->kdb_state = KDB_UNSTABLE; i = (upd->kdb_entry_sno - 1) % ulogentries; - indx_log = INDEX(ulog, i); + indx_log = ulog_index(ulog, i); memset(indx_log, 0, ulog->kdb_block); indx_log->kdb_umagic = KDB_ULOG_MAGIC; @@ -335,7 +361,7 @@ store_update(kdb_log_context *log_ctx, kdb_incr_update_t *upd) } else { /* We are circling; set kdb_first_sno and time to the next update. */ i = upd->kdb_entry_sno % ulogentries; - indx_log = INDEX(ulog, i); + indx_log = ulog_index(ulog, i); ulog->kdb_first_sno = indx_log->kdb_entry_sno; ulog->kdb_first_time = indx_log->kdb_time; } @@ -593,7 +619,7 @@ ulog_get_entries(krb5_context context, const kdb_last_t *last, for (; sno < ulog->kdb_last_sno; sno++) { indx = sno % ulogentries; - indx_log = INDEX(ulog, indx); + indx_log = ulog_index(ulog, indx); memset(upd, 0, sizeof(kdb_incr_update_t)); xdrmem_create(&xdrs, (char *)indx_log->entry_data, diff --git a/src/tests/t_iprop.py b/src/tests/t_iprop.py index b356971db..1f1634f31 100755 --- a/src/tests/t_iprop.py +++ b/src/tests/t_iprop.py @@ -86,8 +86,10 @@ def wait_for_prop(kpropd, full_expected, expected_old, expected_new): # Verify the output of kproplog against the expected number of # entries, first and last serial number, and a list of principal names # for the update entrires. -def check_ulog(num, first, last, entries, env=None): +def check_ulog(num, first, last, entries, env=None, bsize=2048): out = realm.run([kproplog], env=env) + if 'Entry block size : ' + str(bsize) + '\n' not in out: + fail('Expected block size %d' % bsize) if 'Number of entries : ' + str(num) + '\n' not in out: fail('Expected %d entries' % num) if last: @@ -458,8 +460,32 @@ for realm in multidb_realms(kdc_conf=conf, create_user=False, wait_for_prop(kpropd2, True, 6, 1) check_ulog(1, 1, 1, [None], replica2) - # Stop the kprop daemons so we can test kpropd -t. + # Create an update large enough to cause a block resize, and make + # sure that it propagates incrementally. + mark('block resize') + cmd = [kadminl, 'cpw', + '-e', 'aes128-sha1,aes256-sha1,aes128-sha2,aes256-sha2', + '-randkey', '-keepold', pr2] + n = 6 + for i in range(n): + realm.run(cmd) + check_ulog(n + 1, 1, n + 1, [None] + n * [pr2], bsize=4096) + kpropd1.send_signal(signal.SIGUSR1) + wait_for_prop(kpropd1, False, 1, n + 1) + check_ulog(n + 1, 1, n + 1, [None] + n * [pr2], replica1, bsize=4096) + kpropd2.send_signal(signal.SIGUSR1) + wait_for_prop(kpropd2, False, 1, n + 1) + check_ulog(n + 1, 1, n + 1, [None] + n * [pr2], replica2, bsize=4096) + + # Reset the ulog again. + realm.run([kproplog, '-R']) + kpropd1.send_signal(signal.SIGUSR1) + wait_for_prop(kpropd1, True, 7, 1) + kpropd2.send_signal(signal.SIGUSR1) + wait_for_prop(kpropd2, True, 7, 1) realm.stop_kpropd(kpropd1) + + # Stop the kprop daemons so we can test kpropd -t. stop_daemon(kpropd2) stop_daemon(kadmind_proponly) mark('kpropd -t')