#include <config.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include <compat/stdbool.h>
#endif
#if defined(HAVE_STDINT_H)
# include <stdint.h>
#elif defined(HAVE_INTTYPES_H)
# include <inttypes.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sudo_compat.h>
#include <sudo_conf.h>
#include <sudo_debug.h>
#include <sudo_event.h>
#include <sudo_eventlog.h>
#include <sudo_fatal.h>
#include <sudo_gettext.h>
#include <sudo_json.h>
#include <sudo_iolog.h>
#include <sudo_rand.h>
#include <sudo_util.h>
#include <logsrvd.h>
struct logsrvd_info_closure {
InfoMessage **info_msgs;
size_t infolen;
};
static bool
logsrvd_json_log_cb(struct json_container *jsonc, void *v)
{
struct logsrvd_info_closure *closure = v;
struct json_value json_value;
size_t idx;
debug_decl(logsrvd_json_log_cb, SUDO_DEBUG_UTIL);
for (idx = 0; idx < closure->infolen; idx++) {
InfoMessage *info = closure->info_msgs[idx];
switch (info->value_case) {
case INFO_MESSAGE__VALUE_NUMVAL:
json_value.type = JSON_NUMBER;
json_value.u.number = info->u.numval;
if (!sudo_json_add_value(jsonc, info->key, &json_value))
goto bad;
break;
case INFO_MESSAGE__VALUE_STRVAL:
if (info->u.strval == NULL) {
sudo_warnx(U_("%s: protocol error: NULL value found in %s"),
"local", info->key);
break;
}
json_value.type = JSON_STRING;
json_value.u.string = info->u.strval;
if (!sudo_json_add_value(jsonc, info->key, &json_value))
goto bad;
break;
case INFO_MESSAGE__VALUE_STRLISTVAL: {
InfoMessage__StringList *strlist = info->u.strlistval;
size_t n;
if (strlist == NULL) {
sudo_warnx(U_("%s: protocol error: NULL value found in %s"),
"local", info->key);
break;
}
if (!sudo_json_open_array(jsonc, info->key))
goto bad;
for (n = 0; n < strlist->n_strings; n++) {
if (strlist->strings[n] == NULL) {
sudo_warnx(U_("%s: protocol error: NULL value found in %s"),
"local", info->key);
break;
}
json_value.type = JSON_STRING;
json_value.u.string = strlist->strings[n];
if (!sudo_json_add_value(jsonc, NULL, &json_value))
goto bad;
}
if (!sudo_json_close_array(jsonc))
goto bad;
break;
}
case INFO_MESSAGE__VALUE_NUMLISTVAL: {
InfoMessage__NumberList *numlist = info->u.numlistval;
size_t n;
if (numlist == NULL) {
sudo_warnx(U_("%s: protocol error: NULL value found in %s"),
"local", info->key);
break;
}
if (!sudo_json_open_array(jsonc, info->key))
goto bad;
for (n = 0; n < numlist->n_numbers; n++) {
json_value.type = JSON_NUMBER;
json_value.u.number = numlist->numbers[n];
if (!sudo_json_add_value(jsonc, NULL, &json_value))
goto bad;
}
if (!sudo_json_close_array(jsonc))
goto bad;
break;
}
default:
sudo_warnx(U_("unexpected value_case %d in %s from %s"),
info->value_case, "InfoMessage", "local");
break;
}
}
debug_return_bool(true);
bad:
debug_return_bool(false);
}
bool
store_accept_local(const AcceptMessage *msg, const uint8_t *buf, size_t len,
struct connection_closure *closure)
{
struct logsrvd_info_closure info = { msg->info_msgs, msg->n_info_msgs };
bool new_session = closure->evlog == NULL;
struct eventlog *evlog = NULL;
bool ret = false;
debug_decl(store_accept_local, SUDO_DEBUG_UTIL);
evlog = evlog_new(msg->submit_time, msg->info_msgs, msg->n_info_msgs,
closure);
if (evlog == NULL) {
closure->errstr = _("error parsing AcceptMessage");
goto done;
}
if (new_session) {
closure->evlog = evlog;
if (msg->expect_iobufs) {
if (!iolog_init(msg, closure)) {
closure->errstr = _("error creating I/O log");
goto done;
}
closure->log_io = true;
}
} else if (closure->log_io) {
free(evlog->iolog_path);
evlog->iolog_path = strdup(closure->evlog->iolog_path);
if (evlog->iolog_path == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
closure->errstr = _("unable to allocate memory");
goto done;
}
if (closure->evlog->iolog_file != NULL) {
evlog->iolog_file = evlog->iolog_path +
(closure->evlog->iolog_file - closure->evlog->iolog_path);
}
sudo_timespecsub(&evlog->event_time, &closure->evlog->event_time,
&evlog->iolog_offset);
}
if (!eventlog_accept(evlog, 0, logsrvd_json_log_cb, &info)) {
closure->errstr = _("error logging accept event");
goto done;
}
if (new_session && closure->log_io) {
const char *relative_path = closure->evlog->iolog_path +
strlen(logsrvd_conf_iolog_base()) + 1;
if (!fmt_log_id_message(closure->uuid, relative_path, closure))
goto done;
if (sudo_ev_add(closure->evbase, closure->write_ev,
logsrvd_conf_server_timeout(), false) == -1) {
sudo_warnx("%s", U_("unable to add event to queue"));
goto done;
}
}
ret = true;
done:
if (closure->evlog != evlog)
eventlog_free(evlog);
debug_return_bool(ret);
}
bool
store_reject_local(const RejectMessage *msg, const uint8_t *buf, size_t len,
struct connection_closure *closure)
{
struct logsrvd_info_closure info = { msg->info_msgs, msg->n_info_msgs };
struct eventlog *evlog = NULL;
bool ret = false;
debug_decl(store_reject_local, SUDO_DEBUG_UTIL);
evlog = evlog_new(msg->submit_time, msg->info_msgs, msg->n_info_msgs,
closure);
if (evlog == NULL) {
closure->errstr = _("error parsing RejectMessage");
goto done;
}
if (closure->evlog == NULL) {
closure->evlog = evlog;
} else if (closure->log_io) {
free(evlog->iolog_path);
evlog->iolog_path = strdup(closure->evlog->iolog_path);
if (evlog->iolog_path == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
closure->errstr = _("unable to allocate memory");
goto done;
}
if (closure->evlog->iolog_file != NULL) {
evlog->iolog_file = evlog->iolog_path +
(closure->evlog->iolog_file - closure->evlog->iolog_path);
}
sudo_timespecsub(&evlog->event_time, &closure->evlog->event_time,
&evlog->iolog_offset);
}
if (!eventlog_reject(evlog, 0, msg->reason, logsrvd_json_log_cb, &info)) {
closure->errstr = _("error logging reject event");
goto done;
}
ret = true;
done:
if (closure->evlog != evlog)
eventlog_free(evlog);
debug_return_bool(ret);
}
static bool
store_exit_info_json(int dfd, const struct eventlog *evlog)
{
struct json_container jsonc = { 0 };
struct json_value json_value;
struct iovec iov[3];
bool ret = false;
int fd = -1;
off_t pos;
debug_decl(store_exit_info_json, SUDO_DEBUG_UTIL);
if (!sudo_json_init(&jsonc, 4, false, false, false))
goto done;
fd = iolog_openat(dfd, "log.json", O_RDWR);
if (fd == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
"unable to open to %s/log.json", evlog->iolog_path);
if (errno == ENOENT) {
ret = true;
}
goto done;
}
if (sudo_timespecisset(&evlog->run_time)) {
if (!sudo_json_open_object(&jsonc, "run_time"))
goto done;
json_value.type = JSON_NUMBER;
json_value.u.number = evlog->run_time.tv_sec;
if (!sudo_json_add_value(&jsonc, "seconds", &json_value))
goto done;
json_value.type = JSON_NUMBER;
json_value.u.number = evlog->run_time.tv_nsec;
if (!sudo_json_add_value(&jsonc, "nanoseconds", &json_value))
goto done;
if (!sudo_json_close_object(&jsonc))
goto done;
}
if (evlog->signal_name != NULL) {
json_value.type = JSON_STRING;
json_value.u.string = evlog->signal_name;
if (!sudo_json_add_value(&jsonc, "signal", &json_value))
goto done;
json_value.type = JSON_BOOL;
json_value.u.boolean = evlog->dumped_core;
if (!sudo_json_add_value(&jsonc, "dumped_core", &json_value))
goto done;
}
json_value.type = JSON_NUMBER;
json_value.u.number = evlog->exit_value;
if (!sudo_json_add_value(&jsonc, "exit_value", &json_value))
goto done;
pos = lseek(fd, -3, SEEK_END);
if (pos == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
"unable to rewind %s/log.json 3 bytes", evlog->iolog_path);
goto done;
}
iov[0].iov_base = (char *)",";
iov[0].iov_len = 1;
iov[1].iov_base = sudo_json_get_buf(&jsonc);
iov[1].iov_len = sudo_json_get_len(&jsonc);
iov[2].iov_base = (char *)"\n}\n";
iov[2].iov_len = 3;
if (writev(fd, iov, 3) < 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
"unable to write %s/log.json", evlog->iolog_path);
if (lseek(fd, pos, SEEK_SET) != -1) {
ignore_result(write(fd, "\n}\n", 3));
}
goto done;
}
ret = true;
done:
if (fd != -1)
close(fd);
sudo_json_free(&jsonc);
debug_return_bool(ret);
}
bool
store_exit_local(const ExitMessage *msg, const uint8_t *buf, size_t len,
struct connection_closure *closure)
{
struct eventlog *evlog = closure->evlog;
int flags = 0;
debug_decl(store_exit_local, SUDO_DEBUG_UTIL);
if (msg->run_time != NULL) {
evlog->run_time.tv_sec = (time_t)msg->run_time->tv_sec;
evlog->run_time.tv_nsec = (long)msg->run_time->tv_nsec;
}
evlog->exit_value = msg->exit_value;
if (msg->signal[0] != '\0') {
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
"command was killed by SIG%s%s", msg->signal,
msg->dumped_core ? " (core dumped)" : "");
free(evlog->signal_name);
evlog->signal_name = strdup(msg->signal);
if (evlog->signal_name == NULL) {
closure->errstr = _("unable to allocate memory");
debug_return_bool(false);
}
evlog->dumped_core = msg->dumped_core;
} else {
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO,
"command exited with %d", msg->exit_value);
}
if (logsrvd_conf_log_exit()) {
if (!eventlog_exit(evlog, flags)) {
closure->errstr = _("error logging exit event");
debug_return_bool(false);
}
}
if (closure->log_io) {
mode_t mode;
if (!store_exit_info_json(closure->iolog_dir_fd, evlog)) {
closure->errstr = _("error logging exit event");
debug_return_bool(false);
}
mode = logsrvd_conf_iolog_mode();
CLR(mode, S_IWUSR|S_IWGRP|S_IWOTH);
if (fchmodat(closure->iolog_dir_fd, "timing", mode, 0) == -1) {
sudo_warn("chmod 0%o %s/%s", (unsigned int)mode, "timing",
logsrvd_conf_iolog_dir());
}
}
debug_return_bool(true);
}
static char *
decode_log_id(const char *b64_log_id, unsigned char uuid[restrict static 16])
{
unsigned char log_id_buf[PATH_MAX + 16];
char *path, *ret;
size_t len;
debug_decl(decode_log_id, SUDO_DEBUG_UTIL);
len = sudo_base64_decode(b64_log_id, log_id_buf, sizeof(log_id_buf) - 1);
if (len == (size_t)-1)
debug_return_str(NULL);
log_id_buf[len] = '\0';
if (len <= 16)
debug_return_str(NULL);
memcpy(uuid, log_id_buf, 16);
if (memchr(&log_id_buf[16], '\0', len - 16) != NULL) {
sudo_warnx("%s", U_("RestartMessage log_id path has embedded NUL"));
debug_return_str(NULL);
}
path = (char *)&log_id_buf[16];
if (contains_dot_dot(path)) {
sudo_warnx("%s", U_("RestartMessage log_id path traversal attack"));
debug_return_str(NULL);
}
if (asprintf(&ret, "%s/%s", logsrvd_conf_iolog_base(), path) == -1) {
ret = NULL;
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
}
debug_return_str(ret);
}
static bool
verify_iolog_uuid(int dfd, const unsigned char uuid[restrict static 16])
{
unsigned char uuid2[16];
char uuid2_str[37];
bool ret = false;
struct stat sb;
int fd;
debug_decl(verify_iolog_uuid, SUDO_DEBUG_UTIL);
fd = openat(dfd, "uuid", O_RDONLY);
if (fd == -1)
goto done;
if (fstat(fd, &sb) == -1 || sb.st_size != 36)
goto done;
if (read(fd, uuid2_str, sizeof(uuid2_str)) != 36)
goto done;
uuid2_str[36] = '\0';
if (sudo_uuid_from_string(uuid2_str, uuid2) != 0)
goto done;
if (memcmp(uuid, uuid2, sizeof(uuid2)) != 0)
goto done;
ret = true;
done:
if (fd != -1) {
errno = EINVAL;
close(fd);
}
debug_return_bool(ret);
}
bool
store_restart_local(const RestartMessage *msg, const uint8_t *buf, size_t len,
struct connection_closure *closure)
{
struct timespec target;
struct stat sb;
int iofd;
debug_decl(store_restart_local, SUDO_DEBUG_UTIL);
target.tv_sec = (time_t)msg->resume_point->tv_sec;
target.tv_nsec = (long)msg->resume_point->tv_nsec;
closure->evlog = calloc(1, sizeof(*closure->evlog));
if (closure->evlog == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
closure->errstr = _("unable to allocate memory");
goto bad;
}
closure->evlog->iolog_path = decode_log_id(msg->log_id, closure->uuid);
if (closure->evlog->iolog_path == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to parse log_id"));
closure->errstr = _("unable to parse log_id");
goto bad;
}
closure->iolog_dir_fd = iolog_openat(AT_FDCWD,
closure->evlog->iolog_path, O_RDONLY|O_DIRECTORY);
if (closure->iolog_dir_fd == -1) {
sudo_warn("%s", closure->evlog->iolog_path);
goto bad;
}
if (!verify_iolog_uuid(closure->iolog_dir_fd, closure->uuid)) {
sudo_warn("%s/uuid", closure->evlog->iolog_path);
goto bad;
}
if (fstatat(closure->iolog_dir_fd, "timing", &sb, 0) == -1) {
sudo_warn("%s/timing", closure->evlog->iolog_path);
goto bad;
}
if (!ISSET(sb.st_mode, S_IWUSR)) {
sudo_warn(U_("%s: %s"), closure->evlog->iolog_path,
U_("log is already complete, cannot be restarted"));
closure->errstr = _("log is already complete, cannot be restarted");
goto bad;
}
if (!iolog_open_all(closure->iolog_dir_fd, closure->evlog->iolog_path,
closure->iolog_files, "r+"))
goto bad;
for (iofd = 0; iofd < IOFD_MAX; iofd++) {
if (closure->iolog_files[iofd].compressed)
debug_return_bool(iolog_rewrite(&target, closure));
}
if (!iolog_seekto(closure->iolog_dir_fd, closure->evlog->iolog_path,
closure->iolog_files, &closure->elapsed_time, &target))
goto bad;
if (iolog_seek(&closure->iolog_files[IOFD_TIMING], 0, SEEK_CUR) == -1) {
sudo_warn("%s/timing", closure->evlog->iolog_path);
goto bad;
}
debug_return_bool(true);
bad:
if (closure->errstr == NULL)
closure->errstr = _("unable to restart log");
debug_return_bool(false);
}
bool
store_alert_local(const AlertMessage *msg, const uint8_t *buf, size_t len,
struct connection_closure *closure)
{
struct eventlog *evlog = NULL;
struct timespec alert_time;
bool ret = false;
debug_decl(store_alert_local, SUDO_DEBUG_UTIL);
if (msg->info_msgs != NULL && msg->n_info_msgs != 0) {
evlog = evlog_new(NULL, msg->info_msgs, msg->n_info_msgs, closure);
if (evlog == NULL) {
closure->errstr = _("error parsing AlertMessage");
goto done;
}
if (closure->evlog == NULL)
closure->evlog = evlog;
}
alert_time.tv_sec = (time_t)msg->alert_time->tv_sec;
alert_time.tv_nsec = (long)msg->alert_time->tv_nsec;
if (!eventlog_alert(evlog, 0, &alert_time, msg->reason, NULL)) {
closure->errstr = _("error logging alert event");
goto done;
}
ret = true;
done:
if (closure->evlog != evlog)
eventlog_free(evlog);
debug_return_bool(ret);
}
bool
store_iobuf_local(int iofd, const IoBuffer *iobuf, const uint8_t *buf,
size_t buflen, struct connection_closure *closure)
{
const struct eventlog *evlog = closure->evlog;
struct ProtobufCBinaryData data = iobuf->data;
char tbuf[1024], *newbuf = NULL;
const char *errstr;
int len;
debug_decl(store_iobuf_local, SUDO_DEBUG_UTIL);
if (!closure->iolog_files[iofd].enabled) {
if (!iolog_create(iofd, closure))
goto bad;
}
len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09d %zu\n",
iofd, (long long)iobuf->delay->tv_sec, (int)iobuf->delay->tv_nsec,
data.len);
if (len < 0 || len >= ssizeof(tbuf)) {
sudo_warnx(U_("unable to format timing buffer, length %d"), len);
goto bad;
}
if (!logsrvd_conf_iolog_log_passwords()) {
if (!iolog_pwfilt_run(logsrvd_conf_iolog_passprompt_regex(), iofd,
(char *)data.data, data.len, &newbuf))
goto bad;
if (newbuf != NULL)
data.data = (uint8_t *)newbuf;
}
if (iolog_write(&closure->iolog_files[iofd], data.data, data.len,
&errstr) == -1) {
sudo_warnx(U_("%s/%s: %s"), evlog->iolog_path, iolog_fd_to_name(iofd),
errstr);
goto bad;
}
if (iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf,
(size_t)len, &errstr) == -1) {
sudo_warnx(U_("%s/%s: %s"), evlog->iolog_path,
iolog_fd_to_name(IOFD_TIMING), errstr);
goto bad;
}
update_elapsed_time(iobuf->delay, &closure->elapsed_time);
free(newbuf);
debug_return_bool(true);
bad:
free(newbuf);
if (closure->errstr == NULL)
closure->errstr = _("error writing IoBuffer");
debug_return_bool(false);
}
bool
store_winsize_local(const ChangeWindowSize *msg, const uint8_t *buf,
size_t buflen, struct connection_closure *closure)
{
const char *errstr;
char tbuf[1024];
int len;
debug_decl(store_winsize_local, SUDO_DEBUG_UTIL);
len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09d %d %d\n", IO_EVENT_WINSIZE,
(long long)msg->delay->tv_sec, (int)msg->delay->tv_nsec,
msg->rows, msg->cols);
if (len < 0 || len >= ssizeof(tbuf)) {
sudo_warnx(U_("unable to format timing buffer, length %d"), len);
goto bad;
}
if (iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf,
(size_t)len, &errstr) == -1) {
sudo_warnx(U_("%s/%s: %s"), closure->evlog->iolog_path,
iolog_fd_to_name(IOFD_TIMING), errstr);
goto bad;
}
update_elapsed_time(msg->delay, &closure->elapsed_time);
debug_return_bool(true);
bad:
if (closure->errstr == NULL)
closure->errstr = _("error writing ChangeWindowSize");
debug_return_bool(false);
}
bool
store_suspend_local(const CommandSuspend *msg, const uint8_t *buf,
size_t buflen, struct connection_closure *closure)
{
const char *errstr;
char tbuf[1024];
int len;
debug_decl(store_suspend_local, SUDO_DEBUG_UTIL);
len = snprintf(tbuf, sizeof(tbuf), "%d %lld.%09d %s\n", IO_EVENT_SUSPEND,
(long long)msg->delay->tv_sec, (int)msg->delay->tv_nsec,
msg->signal);
if (len < 0 || len >= ssizeof(tbuf)) {
sudo_warnx(U_("unable to format timing buffer, length %d"), len);
goto bad;
}
if (iolog_write(&closure->iolog_files[IOFD_TIMING], tbuf,
(size_t)len, &errstr) == -1) {
sudo_warnx(U_("%s/%s: %s"), closure->evlog->iolog_path,
iolog_fd_to_name(IOFD_TIMING), errstr);
goto bad;
}
update_elapsed_time(msg->delay, &closure->elapsed_time);
debug_return_bool(true);
bad:
if (closure->errstr == NULL)
closure->errstr = _("error writing CommandSuspend");
debug_return_bool(false);
}
struct client_message_switch cms_local = {
store_accept_local,
store_reject_local,
store_exit_local,
store_restart_local,
store_alert_local,
store_iobuf_local,
store_suspend_local,
store_winsize_local
};