/* * Copyright (C) 2013 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "session-utils.h" #include "common/cockpitframe.h" #include "common/cockpitjsonprint.h" #include "common/cockpitmemory.h" #include #include #include #include #include #include #include #ifndef _PATH_BTMP #define _PATH_BTMP "/var/log/btmp" #endif const char *program_name; struct passwd *pwd; pid_t child; int want_session = 1; static char *auth_prefix = NULL; static size_t auth_prefix_size = 0; static char *auth_msg = NULL; static size_t auth_msg_size = 0; static FILE *authf = NULL; char * read_authorize_response (const char *what) { unsigned char *message; debug ("reading %s authorize message", what); ssize_t len = cockpit_frame_read (STDIN_FILENO, &message); if (len < 0) err (EX, "couldn't read %s", what); if (len == 0) { debug ("EOF reading %s authorize message", what); exit (0); } return (char *)message; } char *get_authorize_key (const char *json, const char *key, bool required) { /* * The authorize messages we receive always have this structure: * * \n{"command":"authorize","cookie":"NNN","response":"...",...more entries...} * * We make many assumptions/restrictions here to keep the parser simple. In particular, no extra * whitespace, everything is a string, and no escaped ". */ if (json == NULL || json[0] != '\n' || json[1] != '{') errx (EX, "authorize message doesn't start with \\n{"); char *key_match; int key_match_len = asprintfx (&key_match, "\"%s\":\"", key); const char *match = strstr (json, key_match); free (key_match); if (!match) { if (required) errx (EX, "authorize message missing required key %s", key); debug ("authorize message missing optional key %s", key); return NULL; } /* advance to the start of the value */ match += key_match_len; /* find end of value */ size_t value_len = strcspn (match, "\""); if (match[value_len] != '"') errx (EX, "syntax error, expected closing quote"); return strndup (match, value_len); } void write_control_string (const char *field, const char *str) { cockpit_json_print_string_property (authf, field, str, -1); } void write_control_bool (const char *field, bool val) { cockpit_json_print_bool_property (authf, field, val); } void write_authorize_begin (void) { assert (authf == NULL); assert (auth_msg_size == 0); assert (auth_msg == NULL); debug ("writing auth challenge"); if (auth_prefix) { free (auth_prefix); auth_prefix = NULL; } if (asprintf (&auth_prefix, "\n{\"command\":\"authorize\",\"cookie\":\"session%u%u\"", (unsigned int)getpid(), (unsigned int)time (NULL)) < 0) { errx (EX, "out of memory allocating string"); } auth_prefix_size = strlen (auth_prefix); authf = open_memstream (&auth_msg, &auth_msg_size); if (!authf) err (EX, "failed to open_memstream()"); fprintf (authf, "%s", auth_prefix); } void write_control_end (void) { assert (authf != NULL); fprintf (authf, "}\n"); fflush (authf); fclose (authf); assert (auth_msg_size > 0); assert (auth_msg != NULL); if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)auth_msg, auth_msg_size) < 0) err (EX, "couldn't write auth request"); debug ("finished auth request"); free (auth_msg); auth_msg = NULL; authf = NULL; auth_msg_size = 0; } __attribute__((__noreturn__)) void exit_init_problem (const char *problem, const char *message) { char *payload = NULL; size_t payload_size = 0; debug ("writing init problem %s message %s", problem, message); FILE *stream = open_memstream (&payload, &payload_size); if (!stream) errx (EX, "couldn't allocate memory for message"); fprintf (stream, "\n{\"command\":\"init\",\"version\":1"); cockpit_json_print_string_property (stream, "problem", problem, -1); cockpit_json_print_string_property (stream, "message", message, -1); fprintf (stream, "}"); fclose (stream); if (cockpit_frame_write (STDOUT_FILENO, (unsigned char *)payload, payload_size) < 0) err (EX, "couldn't write init message"); free (payload); exit (5); } void build_string (char **buf, size_t *size, const char *str, size_t len) { if (*size == 0) return; if (len > *size - 1) len = *size - 1; memcpy (*buf, str, len); (*buf)[len] = '\0'; *buf += len; *size -= len; } static bool do_lastlog (uid_t uid, const struct timeval *now, const char *rhost, time_t *out_last_login, FILE *messages) { struct lastlog entry; bool result = false; int fd = -1; ssize_t r; fd = open (_PATH_LASTLOG, O_RDWR); if (fd == -1) { warn ("failed to open %s", _PATH_LASTLOG); goto out; } r = pread (fd, &entry, sizeof entry, uid * sizeof entry); if (r == sizeof entry && entry.ll_time != 0) { /* got an entry for the user */ /* the ll_host and ll_line fields can be nul-terminated, but they * can also extend to the full length of the field without * nul-termination. use the maxlen parameter to help with that. */ if (!cockpit_json_print_integer_property (messages, "last-login-time", entry.ll_time) || !cockpit_json_print_string_property (messages, "last-login-host", entry.ll_host, UT_HOSTSIZE) || !cockpit_json_print_string_property (messages, "last-login-line", entry.ll_line, UT_LINESIZE)) { warnx ("failed to print last-login details to messages memfd"); goto out; } if (out_last_login) *out_last_login = entry.ll_time; } else if (r == sizeof entry) { /* read the entry, but it's nul. user never logged in. */ *out_last_login = 0; } else if (r == 0) { /* no such entry in file: never logged in? */ *out_last_login = 0; } else if (r < 0) { /* error */ warn ("failed to pread() %s for uid %u", _PATH_LASTLOG, (unsigned) uid); goto out; } else { /* some other size (incomplete read) */ warnx ("incomplete pread() %s for uid %u: %zu of %zu bytes", _PATH_LASTLOG, (unsigned) uid, r, sizeof entry); goto out; } /* XXX: We'd really like to use strncpy() here, which is perfectly * designed for what we need to do: copy a string up to N characters * into a fixed width field, adding nul bytes if the string is shorter * than N. * * Unfortunately, when you use it in this way, GCC is convinced that * you don't know what you're doing and gives a warning that's very * difficult to get rid of. We tried using #pragma here before, but * after several attempts, it was difficult to get the * conditionalising (for the compiler version) correct. * * Let's just nul out the struct and use memcpy(). Sigh. * * strncpy (entry.ll_host, rhost, sizeof entry.ll_host); * strncpy (entry.ll_line, "web console", sizeof entry.ll_line); * * See also https://gcc.gnu.org/bugzilla/show_bug.cgi?id=94615 * See also https://sourceware.org/bugzilla/show_bug.cgi?id=25844 */ memset (&entry, 0, sizeof entry); memcpy (entry.ll_host, rhost, MIN (strlen (rhost), sizeof entry.ll_host)); const char * const line = "web console"; memcpy (entry.ll_line, line, MIN (strlen (line), sizeof entry.ll_line)); entry.ll_time = now->tv_sec; r = pwrite (fd, &entry, sizeof entry, uid * sizeof entry); if (r == -1) { /* error */ warn ("failed to pwrite() %s for uid %u", _PATH_LASTLOG, (unsigned) uid); goto out; } else if (r != sizeof entry) { /* incomplete write */ warnx ("incomplete pwrite() %s for uid %u: %zu or %zu bytes", _PATH_LASTLOG, (unsigned) uid, r, sizeof entry); goto out; } result = true; out: if (fd != -1) close (fd); return result; } static bool scan_btmp (const char *username, time_t last_success, FILE *messages) { bool success = false; int fail_count = 0; struct utmp last; int fd; fd = open (_PATH_BTMP, O_RDONLY | O_CLOEXEC); if (fd == -1) { if (errno == ENOENT) { /* no btmp → no failed attempts */ success = true; goto out; } warn ("open(%s) failed", _PATH_BTMP); goto out; } while (true) { struct utmp entry; ssize_t r; do r = read (fd, &entry, sizeof entry); while (r == -1 && errno == EINTR); if (r == 0) break; if (r < 0) { warn ("read(%s) failed", _PATH_BTMP); goto out; } if (r != sizeof entry) { warnx ("read(%s) returned partial result (%zu of %zu bytes)", _PATH_BTMP, r, sizeof entry); goto out; } if (entry.ut_tv.tv_sec > last_success && strncmp (entry.ut_user, username, sizeof entry.ut_user) == 0) { last = entry; fail_count++; } } if (fail_count == 0) { success = true; goto out; } /* only print messages if we actually have failures */ success = cockpit_json_print_integer_property (messages, "fail-count", fail_count) && cockpit_json_print_integer_property (messages, "last-fail-time", last.ut_tv.tv_sec) && cockpit_json_print_string_property (messages, "last-fail-host", last.ut_host, UT_HOSTSIZE) && cockpit_json_print_string_property (messages, "last-fail-line", last.ut_line, UT_LINESIZE); out: if (fd > -1) close (fd); return success; } void utmp_log (int login, const char *rhost, FILE *messages) { char id[UT_LINESIZE + 1]; struct utmp ut; struct timeval tv; int pid; pid = getpid (); snprintf (id, UT_LINESIZE, "%d", pid); assert (pwd != NULL); utmpname (_PATH_UTMP); setutent (); memset (&ut, 0, sizeof(ut)); strncpy (ut.ut_id, id, sizeof (ut.ut_id)); ut.ut_id[sizeof (ut.ut_id) - 1] = 0; strncpy (ut.ut_line, "web console", sizeof ut.ut_line); ut.ut_line[sizeof ut.ut_line - 1] = 0; if (login) { strncpy (ut.ut_user, pwd->pw_name, sizeof(ut.ut_user)); ut.ut_user[sizeof (ut.ut_user) - 1] = 0; strncpy (ut.ut_host, rhost, sizeof(ut.ut_host)); ut.ut_host[sizeof (ut.ut_host) - 1] = 0; } gettimeofday (&tv, NULL); ut.ut_tv.tv_sec = tv.tv_sec; ut.ut_tv.tv_usec = tv.tv_usec; ut.ut_type = login ? USER_PROCESS : DEAD_PROCESS; ut.ut_pid = pid; pututline (&ut); endutent (); updwtmp (_PATH_WTMP, &ut); if (login) { time_t last_success; if (do_lastlog (pwd->pw_uid, &tv, rhost, &last_success, messages)) scan_btmp (pwd->pw_name, last_success, messages); } } void btmp_log (const char *username, const char *rhost) { struct timeval tv; /* the `tv` in the utmp struct is not actually a `struct timeval`, so * we need to read into a temporary variable and then copy the fields. */ gettimeofday (&tv, NULL); struct utmp entry = { .ut_line = "web console", .ut_pid = getpid (), .ut_tv.tv_sec = tv.tv_sec, .ut_tv.tv_usec = tv.tv_usec, .ut_type = LOGIN_PROCESS, }; /* see utmp(5), it is ok to not null-terminate these if they have maximum size */ /* add coverity markers for older glibcs: https://sourceware.org/bugzilla/show_bug.cgi?id=24899 */ /* coverity[buffer_size_warning : FALSE] */ strncpy (entry.ut_host, rhost, sizeof entry.ut_host); /* coverity[buffer_size_warning : FALSE] */ strncpy (entry.ut_user, username, sizeof entry.ut_user); int fd = open (_PATH_BTMP, O_WRONLY | O_APPEND); if (fd == -1) { warn ("open(%s) failed", _PATH_BTMP); goto out; } ssize_t r = write (fd, &entry, sizeof entry); if (r < 0) { warn ("write() %s failed", _PATH_BTMP); goto out; } else if (r != sizeof entry) { warnx ("incomplete write() %s: %zu of %zu bytes", _PATH_BTMP, r, sizeof entry); goto out; } out: if (fd != -1) close (fd); } void authorize_logger (const char *data) { warnx ("%s", data); } /* signal- and after-fork()-safe function to format a string, print it * to stderr and abort execution. Never returns. */ static noreturn void __attribute__ ((format (printf, 1, 2))) abort_with_message (const char *format, ...) { char buffer[1024]; va_list ap; va_start (ap, format); size_t length = vsnprintf (buffer, sizeof buffer, format, ap); va_end (ap); size_t ofs = 0; while (ofs != length) { ssize_t r; do r = write (STDERR_FILENO, buffer + ofs, length - ofs); while (r == -1 && errno == EINTR); if (0 <= r && r <= length - ofs) ofs += r; else break; /* something went wrong, but we can't deal with it */ } abort (); } /* signal- and after-fork()-safe function to remap file descriptors * according to a specified array. All other file descriptors are * closed. * * Commonly used after fork() and before exec(). */ static void fd_remap (const int *remap_fds, int n_remap_fds) { if (n_remap_fds < 0 || n_remap_fds > 1024) abort_with_message ("requested to fd_remap() too many fds!"); int *fds = alloca (sizeof (int) * n_remap_fds); memcpy (fds, remap_fds, sizeof (int) * n_remap_fds); /* we need to get all of the remap-fds to be numerically above * n_remap_fds in order to make sure that we don't overwrite them in * the middle of the dup2() loop below, and also avoid the case that * dup2() is a no-op (which could fail to clear the O_CLOEXEC flag, * for example). */ for (int i = 0; i < n_remap_fds; i++) if (fds[i] != -1 && fds[i] < n_remap_fds) { int new_fd = fcntl (fds[i], F_DUPFD, n_remap_fds); /* returns >= n_remap_fds */ if (new_fd == -1) abort_with_message ("fcntl(%d, F_DUPFD) failed: %m", fds[i]); fds[i] = new_fd; } /* now we can map the fds into their final spot */ for (int i = 0; i < n_remap_fds; i++) if (fds[i] != -1) /* no-op */ if (dup2 (fds[i], i) != i) abort_with_message ("dup2(%d, %d) failed: %m", fds[i], i); /* close everything else */ close_range (n_remap_fds, ~0U, 0); } int spawn_and_wait (const char **argv, const char **envp, const int *remap_fds, int n_remap_fds, uid_t uid, gid_t gid) { pid_t child; child = fork (); if (child == -1) abort_with_message ("cockpit-session: fork() failed: %m"); if (child == 0) { /* This is the child process. Do preparation, and exec(). */ if (setresgid (gid, gid, gid) != 0) abort_with_message ("setresgid: couldn't set gid to %u: %m\n", (int) gid); if (setresuid (uid, uid, uid) != 0) abort_with_message ("setresuid: couldn't set uid to %u: %m\n", (int) uid); /* paranoid */ { uid_t real, effective, saved; int r; r = getresuid (&real, &effective, &saved); assert (r == 0 && real == uid && effective == uid && saved == uid); } { gid_t real, effective, saved; int r; r = getresgid (&real, &effective, &saved); assert (r == 0 && real == gid && effective == gid && saved == gid); } if (n_remap_fds != -1) fd_remap (remap_fds, n_remap_fds); execvpe (argv[0], (char **) argv, (char **) envp); _exit(127); } else { /* This is the parent process. Wait for the child to exit. */ int wstatus; int r; do r = waitpid (child, &wstatus, 0); while (r == -1 && errno == EINTR); if (r == -1) abort_with_message ("waitpid(%d) on cockpit-bridge process failed: %m", (int) child); /* 0 can only be returned of WNOHANG was given */ assert (r == child); return wstatus; } }