diff options
Diffstat (limited to 'sshd.c')
-rw-r--r-- | sshd.c | 428 |
1 files changed, 368 insertions, 60 deletions
@@ -1,4 +1,4 @@ -/* $OpenBSD: sshd.c,v 1.605 2024/06/01 07:03:37 djm Exp $ */ +/* $OpenBSD: sshd.c,v 1.606 2024/06/06 17:15:25 djm Exp $ */ /* * Copyright (c) 2000, 2001, 2002 Markus Friedl. All rights reserved. * Copyright (c) 2002 Niels Provos. All rights reserved. @@ -89,6 +89,7 @@ #include "version.h" #include "ssherr.h" #include "sk-api.h" +#include "addr.h" #include "srclimit.h" /* Re-exec fds */ @@ -138,6 +139,8 @@ struct { } sensitive_data; /* This is set to true when a signal is received. */ +static volatile sig_atomic_t received_siginfo = 0; +static volatile sig_atomic_t received_sigchld = 0; static volatile sig_atomic_t received_sighup = 0; static volatile sig_atomic_t received_sigterm = 0; @@ -145,8 +148,9 @@ static volatile sig_atomic_t received_sigterm = 0; u_int utmp_len = HOST_NAME_MAX+1; /* - * startup_pipes/flags are used for tracking children of the listening sshd - * process early in their lifespans. This tracking is needed for three things: + * The early_child/children array below is used for tracking children of the + * listening sshd process early in their lifespans, before they have + * completed authentication. This tracking is needed for four things: * * 1) Implementing the MaxStartups limit of concurrent unauthenticated * connections. @@ -155,14 +159,31 @@ u_int utmp_len = HOST_NAME_MAX+1; * after it restarts. * 3) Ensuring that rexec'd sshd processes have received their initial state * from the parent listen process before handling SIGHUP. + * 4) Tracking and logging unsuccessful exits from the preauth sshd monitor, + * including and especially those for LoginGraceTime timeouts. * * Child processes signal that they have completed closure of the listen_socks * and (if applicable) received their rexec state by sending a char over their - * sock. Child processes signal that authentication has completed by closing - * the sock (or by exiting). + * sock. + * + * Child processes signal that authentication has completed by sending a + * second char over the socket before closing it, otherwise the listener will + * continue tracking the child (and using up a MaxStartups slot) until the + * preauth subprocess exits, whereupon the listener will log its exit status. + * preauth processes will exit with a status of EXIT_LOGIN_GRACE to indicate + * they did not authenticate before the LoginGraceTime alarm fired. */ -static int *startup_pipes = NULL; -static int *startup_flags = NULL; /* Indicates child closed listener */ +struct early_child { + int pipefd; + int early; /* Indicates child closed listener */ + char *id; /* human readable connection identifier */ + pid_t pid; + struct xaddr addr; + int have_addr; + int status, have_status; +}; +static struct early_child *children; +static int children_active; static int startup_pipe = -1; /* in child */ /* sshd_config buffer */ @@ -192,15 +213,257 @@ close_listen_socks(void) num_listen_socks = 0; } +/* Allocate and initialise the children array */ +static void +child_alloc(void) +{ + int i; + + children = xcalloc(options.max_startups, sizeof(*children)); + for (i = 0; i < options.max_startups; i++) { + children[i].pipefd = -1; + children[i].pid = -1; + } +} + +/* Register a new connection in the children array; child pid comes later */ +static struct early_child * +child_register(int pipefd, int sockfd) +{ + int i, lport, rport; + char *laddr = NULL, *raddr = NULL; + struct early_child *child = NULL; + struct sockaddr_storage addr; + socklen_t addrlen = sizeof(addr); + struct sockaddr *sa = (struct sockaddr *)&addr; + + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd != -1 || children[i].pid > 0) + continue; + child = &(children[i]); + break; + } + if (child == NULL) { + fatal_f("error: accepted connection when all %d child " + " slots full", options.max_startups); + } + child->pipefd = pipefd; + child->early = 1; + /* record peer address, if available */ + if (getpeername(sockfd, sa, &addrlen) == 0 && + addr_sa_to_xaddr(sa, addrlen, &child->addr) == 0) + child->have_addr = 1; + /* format peer address string for logs */ + if ((lport = get_local_port(sockfd)) == 0 || + (rport = get_peer_port(sockfd)) == 0) { + /* Not a TCP socket */ + raddr = get_peer_ipaddr(sockfd); + xasprintf(&child->id, "connection from %s", raddr); + } else { + laddr = get_local_ipaddr(sockfd); + raddr = get_peer_ipaddr(sockfd); + xasprintf(&child->id, "connection from %s to %s", laddr, raddr); + } + free(laddr); + free(raddr); + if (++children_active > options.max_startups) + fatal_f("internal error: more children than max_startups"); + + return child; +} + +/* + * Finally free a child entry. Don't call this directly. + */ +static void +child_finish(struct early_child *child) +{ + if (children_active == 0) + fatal_f("internal error: children_active underflow"); + if (child->pipefd != -1) + close(child->pipefd); + free(child->id); + memset(child, '\0', sizeof(*child)); + child->pipefd = -1; + child->pid = -1; + children_active--; +} + +/* + * Close a child's pipe. This will not stop tracking the child immediately + * (it will still be tracked for waitpid()) unless force_final is set, or + * child has already exited. + */ +static void +child_close(struct early_child *child, int force_final, int quiet) +{ + if (!quiet) + debug_f("enter%s", force_final ? " (forcing)" : ""); + if (child->pipefd != -1) { + close(child->pipefd); + child->pipefd = -1; + } + if (child->pid == -1 || force_final) + child_finish(child); +} + +/* Record a child exit. Safe to call from signal handlers */ +static void +child_exit(pid_t pid, int status) +{ + int i; + + if (children == NULL || pid <= 0) + return; + for (i = 0; i < options.max_startups; i++) { + if (children[i].pid == pid) { + children[i].have_status = 1; + children[i].status = status; + break; + } + } +} + +/* + * Reap a child entry that has exited, as previously flagged + * using child_exit(). + * Handles logging of exit condition and will finalise the child if its pipe + * had already been closed. + */ +static void +child_reap(struct early_child *child) +{ + LogLevel level = SYSLOG_LEVEL_DEBUG1; + int was_crash, penalty_type = SRCLIMIT_PENALTY_NONE; + + /* Log exit information */ + if (WIFSIGNALED(child->status)) { + /* + * Increase logging for signals potentially associated + * with serious conditions. + */ + if ((was_crash = signal_is_crash(WTERMSIG(child->status)))) + level = SYSLOG_LEVEL_ERROR; + do_log2(level, "session process %ld for %s killed by " + "signal %d%s", (long)child->pid, child->id, + WTERMSIG(child->status), child->early ? " (early)" : ""); + if (was_crash) + penalty_type = SRCLIMIT_PENALTY_CRASH; + } else if (!WIFEXITED(child->status)) { + penalty_type = SRCLIMIT_PENALTY_CRASH; + error("session process %ld for %s terminated abnormally, " + "status=0x%x%s", (long)child->pid, child->id, child->status, + child->early ? " (early)" : ""); + } else { + /* Normal exit. We care about the status */ + switch (WEXITSTATUS(child->status)) { + case 0: + debug3_f("preauth child %ld for %s completed " + "normally %s", (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + case EXIT_LOGIN_GRACE: + penalty_type = SRCLIMIT_PENALTY_GRACE_EXCEEDED; + logit("Timeout before authentication for %s, " + "pid = %ld%s", child->id, (long)child->pid, + child->early ? " (early)" : ""); + break; + case EXIT_CHILD_CRASH: + penalty_type = SRCLIMIT_PENALTY_CRASH; + logit("Session process %ld unpriv child crash for %s%s", + (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + case EXIT_AUTH_ATTEMPTED: + penalty_type = SRCLIMIT_PENALTY_AUTHFAIL; + debug_f("preauth child %ld for %s exited " + "after unsuccessful auth attempt %s", + (long)child->pid, child->id, + child->early ? " (early)" : ""); + break; + default: + penalty_type = SRCLIMIT_PENALTY_NOAUTH; + debug_f("preauth child %ld for %s exited " + "with status %d%s", (long)child->pid, child->id, + WEXITSTATUS(child->status), + child->early ? " (early)" : ""); + break; + } + } + /* + * XXX would be nice to have more subtlety here. + * - Different penalties + * a) authentication failures without success (e.g. brute force) + * b) login grace exceeded (penalise DoS) + * c) monitor crash (penalise exploit attempt) + * d) unpriv preauth crash (penalise exploit attempt) + * - Unpriv auth exit status/WIFSIGNALLED is not available because + * the "mm_request_receive: monitor fd closed" fatal kills the + * monitor before waitpid() can occur. It would be good to use the + * unpriv exit status to detect crashes. + * + * For now, just penalise (a), (b) and (c), since that is what we have + * readily available. The authentication failures detection cannot + * discern between failed authentication and other connection problems + * until we have the unpriv exist status plumbed through (and the unpriv + * child modified to use a different exit status when auth has been + * attempted), but it's a start. + */ + if (child->have_addr) + srclimit_penalise(&child->addr, penalty_type); + + child->pid = -1; + child->have_status = 0; + if (child->pipefd == -1) + child_finish(child); +} + +/* Reap all children that have exited; called after SIGCHLD */ +static void +child_reap_all_exited(void) +{ + int i; + + if (children == NULL) + return; + for (i = 0; i < options.max_startups; i++) { + if (!children[i].have_status) + continue; + child_reap(&(children[i])); + } +} + static void close_startup_pipes(void) { int i; - if (startup_pipes) - for (i = 0; i < options.max_startups; i++) - if (startup_pipes[i] != -1) - close(startup_pipes[i]); + if (children == NULL) + return; + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd != -1) + child_close(&(children[i]), 1, 1); + } +} + +/* Called after SIGINFO */ +static void +show_info(void) +{ + int i; + + /* XXX print listening sockets here too */ + if (children == NULL) + return; + logit("%d active startups", children_active); + for (i = 0; i < options.max_startups; i++) { + if (children[i].pipefd == -1 && children[i].pid <= 0) + continue; + logit("child %d: fd=%d pid=%ld %s%s", i, children[i].pipefd, + (long)children[i].pid, children[i].id, + children[i].early ? " (early)" : ""); + } + srclimit_penalty_info(); } /* @@ -244,6 +507,14 @@ sigterm_handler(int sig) received_sigterm = sig; } +#ifdef SIGINFO +static void +siginfo_handler(int sig) +{ + received_siginfo = 1; +} +#endif + /* * SIGCHLD handler. This is called whenever a child dies. This will then * reap any zombies left by exited children. @@ -255,9 +526,17 @@ main_sigchld_handler(int sig) pid_t pid; int status; - while ((pid = waitpid(-1, &status, WNOHANG)) > 0 || - (pid == -1 && errno == EINTR)) - ; + for (;;) { + if ((pid = waitpid(-1, &status, WNOHANG)) == 0) + break; + else if (pid == -1) { + if (errno == EINTR) + continue; + break; + } + child_exit(pid, status); + received_sigchld = 1; + } errno = save_errno; } @@ -290,7 +569,7 @@ should_drop_connection(int startups) } /* - * Check whether connection should be accepted by MaxStartups. + * Check whether connection should be accepted by MaxStartups or for penalty. * Returns 0 if the connection is accepted. If the connection is refused, * returns 1 and attempts to send notification to client. * Logs when the MaxStartups condition is entered or exited, and periodically @@ -300,12 +579,17 @@ static int drop_connection(int sock, int startups, int notify_pipe) { char *laddr, *raddr; - const char msg[] = "Exceeded MaxStartups\r\n"; + const char *reason = NULL, msg[] = "Not allowed at this time\r\n"; static time_t last_drop, first_drop; static u_int ndropped; LogLevel drop_level = SYSLOG_LEVEL_VERBOSE; time_t now; + if (!srclimit_penalty_check_allow(sock, &reason)) { + drop_level = SYSLOG_LEVEL_INFO; + goto handle; + } + now = monotime(); if (!should_drop_connection(startups) && srclimit_check_allow(sock, notify_pipe) == 1) { @@ -335,12 +619,16 @@ drop_connection(int sock, int startups, int notify_pipe) } last_drop = now; ndropped++; + reason = "past Maxstartups"; + handle: laddr = get_local_ipaddr(sock); raddr = get_peer_ipaddr(sock); - do_log2(drop_level, "drop connection #%d from [%s]:%d on [%s]:%d " - "past MaxStartups", startups, raddr, get_peer_port(sock), - laddr, get_local_port(sock)); + do_log2(drop_level, "drop connection #%d from [%s]:%d on [%s]:%d %s", + startups, + raddr, get_peer_port(sock), + laddr, get_local_port(sock), + reason); free(laddr); free(raddr); /* best-effort notification to client */ @@ -547,8 +835,12 @@ server_listen(void) u_int i; /* Initialise per-source limit tracking. */ - srclimit_init(options.max_startups, options.per_source_max_startups, - options.per_source_masklen_ipv4, options.per_source_masklen_ipv6); + srclimit_init(options.max_startups, + options.per_source_max_startups, + options.per_source_masklen_ipv4, + options.per_source_masklen_ipv6, + &options.per_source_penalty, + options.per_source_penalty_exempt); for (i = 0; i < options.num_listen_addrs; i++) { listen_on_addrs(&options.listen_addrs[i]); @@ -574,32 +866,32 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, int log_stderr) { struct pollfd *pfd = NULL; - int i, j, ret, npfd; - int ostartups = -1, startups = 0, listening = 0, lameduck = 0; + int i, ret, npfd; + int oactive = -1, listening = 0, lameduck = 0; int startup_p[2] = { -1 , -1 }, *startup_pollfd; char c = 0; struct sockaddr_storage from; + struct early_child *child; socklen_t fromlen; - pid_t pid; u_char rnd[256]; sigset_t nsigset, osigset; /* pipes connected to unauthenticated child sshd processes */ - startup_pipes = xcalloc(options.max_startups, sizeof(int)); - startup_flags = xcalloc(options.max_startups, sizeof(int)); + child_alloc(); startup_pollfd = xcalloc(options.max_startups, sizeof(int)); - for (i = 0; i < options.max_startups; i++) - startup_pipes[i] = -1; /* * Prepare signal mask that we use to block signals that might set - * received_sigterm or received_sighup, so that we are guaranteed + * received_sigterm/hup/chld/info, so that we are guaranteed * to immediately wake up the ppoll if a signal is received after * the flag is checked. */ sigemptyset(&nsigset); sigaddset(&nsigset, SIGHUP); sigaddset(&nsigset, SIGCHLD); +#ifdef SIGINFO + sigaddset(&nsigset, SIGINFO); +#endif sigaddset(&nsigset, SIGTERM); sigaddset(&nsigset, SIGQUIT); @@ -621,11 +913,19 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, unlink(options.pid_file); exit(received_sigterm == SIGTERM ? 0 : 255); } - if (ostartups != startups) { + if (received_sigchld) { + child_reap_all_exited(); + received_sigchld = 0; + } + if (received_siginfo) { + show_info(); + received_siginfo = 0; + } + if (oactive != children_active) { setproctitle("%s [listener] %d of %d-%d startups", - listener_proctitle, startups, + listener_proctitle, children_active, options.max_startups_begin, options.max_startups); - ostartups = startups; + oactive = children_active; } if (received_sighup) { if (!lameduck) { @@ -646,8 +946,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, npfd = num_listen_socks; for (i = 0; i < options.max_startups; i++) { startup_pollfd[i] = -1; - if (startup_pipes[i] != -1) { - pfd[npfd].fd = startup_pipes[i]; + if (children[i].pipefd != -1) { + pfd[npfd].fd = children[i].pipefd; pfd[npfd].events = POLLIN; startup_pollfd[i] = npfd++; } @@ -665,34 +965,46 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, continue; for (i = 0; i < options.max_startups; i++) { - if (startup_pipes[i] == -1 || + if (children[i].pipefd == -1 || startup_pollfd[i] == -1 || !(pfd[startup_pollfd[i]].revents & (POLLIN|POLLHUP))) continue; - switch (read(startup_pipes[i], &c, sizeof(c))) { + switch (read(children[i].pipefd, &c, sizeof(c))) { case -1: if (errno == EINTR || errno == EAGAIN) continue; if (errno != EPIPE) { error_f("startup pipe %d (fd=%d): " - "read %s", i, startup_pipes[i], + "read %s", i, children[i].pipefd, strerror(errno)); } /* FALLTHROUGH */ case 0: - /* child exited or completed auth */ - close(startup_pipes[i]); - srclimit_done(startup_pipes[i]); - startup_pipes[i] = -1; - startups--; - if (startup_flags[i]) + /* child exited preauth */ + if (children[i].early) listening--; + srclimit_done(children[i].pipefd); + child_close(&(children[i]), 0, 0); break; case 1: - /* child has finished preliminaries */ - if (startup_flags[i]) { + if (children[i].early && c == '\0') { + /* child has finished preliminaries */ listening--; - startup_flags[i] = 0; + children[i].early = 0; + debug2_f("child %lu for %s received " + "config", (long)children[i].pid, + children[i].id); + } else if (!children[i].early && c == '\001') { + /* child has completed auth */ + debug2_f("child %lu for %s auth done", + (long)children[i].pid, + children[i].id); + child_close(&(children[i]), 1, 0); + } else { + error_f("unexpected message 0x%02x " + "child %ld for %s in state %d", + (int)c, (long)children[i].pid, + children[i].id, children[i].early); } break; } @@ -721,7 +1033,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, close(*newsock); continue; } - if (drop_connection(*newsock, startups, startup_p[0])) { + if (drop_connection(*newsock, + children_active, startup_p[0])) { close(*newsock); close(startup_p[0]); close(startup_p[1]); @@ -738,14 +1051,6 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, continue; } - for (j = 0; j < options.max_startups; j++) - if (startup_pipes[j] == -1) { - startup_pipes[j] = startup_p[0]; - startups++; - startup_flags[j] = 1; - break; - } - /* * Got connection. Fork a child to handle it, unless * we are in debugging mode. @@ -763,7 +1068,6 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, close(startup_p[0]); close(startup_p[1]); startup_pipe = -1; - pid = getpid(); send_rexec_state(config_s[0], cfg); close(config_s[0]); free(pfd); @@ -777,7 +1081,8 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, */ platform_pre_fork(); listening++; - if ((pid = fork()) == 0) { + child = child_register(startup_p[0], *newsock); + if ((child->pid = fork()) == 0) { /* * Child. Close the listening and * max_startup sockets. Start using @@ -802,11 +1107,11 @@ server_accept_loop(int *sock_in, int *sock_out, int *newsock, int *config_s, } /* Parent. Stay in the loop. */ - platform_post_fork_parent(pid); - if (pid == -1) + platform_post_fork_parent(child->pid); + if (child->pid == -1) error("fork: %.100s", strerror(errno)); else - debug("Forked child %ld.", (long)pid); + debug("Forked child %ld.", (long)child->pid); close(startup_p[1]); @@ -1428,6 +1733,9 @@ main(int ac, char **av) ssh_signal(SIGCHLD, main_sigchld_handler); ssh_signal(SIGTERM, sigterm_handler); ssh_signal(SIGQUIT, sigterm_handler); +#ifdef SIGINFO + ssh_signal(SIGINFO, siginfo_handler); +#endif platform_post_listen(); |