X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/1fa1333ca1a744ce7fa8af3fc73872b082631fe3..8f2cf8f5adaa08ef84b47bf9bc2f71e39236c22d:/src/src/log.c diff --git a/src/src/log.c b/src/src/log.c index 0aaf94a31..7a73b154a 100644 --- a/src/src/log.c +++ b/src/src/log.c @@ -3,6 +3,7 @@ *************************************************/ /* Copyright (c) University of Cambridge 1995 - 2018 */ +/* Copyright (c) The Exim Maintainers 2020 - 2021 */ /* See the file NOTICE for conditions of use and distribution. */ /* Functions for writing log files. The code for maintaining datestamped @@ -104,7 +105,8 @@ static const uschar * exim_errstrings[] = { US"Negotiation failed for proxy configured host", US"Authenticator 'other' failure", US"target not supporting SMTPUTF8", - US"", + US"host is local", + US"tainted filename", US"Not time for routing", US"Not time for local delivery", @@ -242,7 +244,7 @@ if (s1) } if (f.receive_call_bombout) receive_bomb_out(NULL, s2); /* does not return */ if (smtp_input) smtp_closedown(s2); -exim_exit(EXIT_FAILURE, NULL); +exim_exit(EXIT_FAILURE); } @@ -263,14 +265,19 @@ overwrite it temporarily if it is necessary to create the directory. Returns: a file descriptor, or < 0 on failure (errno set) */ -int -log_create(uschar *name) +static int +log_open_already_exim(uschar * const name) { -int fd = Uopen(name, -#ifdef O_CLOEXEC - O_CLOEXEC | -#endif - O_CREAT|O_APPEND|O_WRONLY, LOG_MODE); +int fd = -1; +const int flags = O_WRONLY | O_APPEND | O_CREAT | O_NONBLOCK; + +if (geteuid() != exim_uid) + { + errno = EACCES; + return -1; + } + +fd = Uopen(name, flags, LOG_MODE); /* If creation failed, attempt to build a log directory in case that is the problem. */ @@ -284,11 +291,7 @@ if (fd < 0 && errno == ENOENT) DEBUG(D_any) debug_printf("%s log directory %s\n", created ? "created" : "failed to create", name); *lastslash = '/'; - if (created) fd = Uopen(name, -#ifdef O_CLOEXEC - O_CLOEXEC | -#endif - O_CREAT|O_APPEND|O_WRONLY, LOG_MODE); + if (created) fd = Uopen(name, flags, LOG_MODE); } return fd; @@ -296,6 +299,79 @@ return fd; +/* Inspired by OpenSSH's mm_send_fd(). Thanks! +Send fd over socketpair. +Return: true iff good. +*/ + +static BOOL +log_send_fd(const int sock, const int fd) +{ +struct msghdr msg; +union { + struct cmsghdr hdr; + char buf[CMSG_SPACE(sizeof(int))]; +} cmsgbuf; +struct cmsghdr *cmsg; +char ch = 'A'; +struct iovec vec = {.iov_base = &ch, .iov_len = 1}; +ssize_t n; + +memset(&msg, 0, sizeof(msg)); +memset(&cmsgbuf, 0, sizeof(cmsgbuf)); +msg.msg_control = &cmsgbuf.buf; +msg.msg_controllen = sizeof(cmsgbuf.buf); + +cmsg = CMSG_FIRSTHDR(&msg); +cmsg->cmsg_len = CMSG_LEN(sizeof(int)); +cmsg->cmsg_level = SOL_SOCKET; +cmsg->cmsg_type = SCM_RIGHTS; +*(int *)CMSG_DATA(cmsg) = fd; + +msg.msg_iov = &vec; +msg.msg_iovlen = 1; + +while ((n = sendmsg(sock, &msg, 0)) == -1 && errno == EINTR); +return n == 1; +} + +/* Inspired by OpenSSH's mm_receive_fd(). Thanks! +Return fd passed over socketpair, or -1 on error. +*/ + +static int +log_recv_fd(const int sock) +{ +struct msghdr msg; +union { + struct cmsghdr hdr; + char buf[CMSG_SPACE(sizeof(int))]; +} cmsgbuf; +struct cmsghdr *cmsg; +char ch = '\0'; +struct iovec vec = {.iov_base = &ch, .iov_len = 1}; +ssize_t n; +int fd; + +memset(&msg, 0, sizeof(msg)); +msg.msg_iov = &vec; +msg.msg_iovlen = 1; + +memset(&cmsgbuf, 0, sizeof(cmsgbuf)); +msg.msg_control = &cmsgbuf.buf; +msg.msg_controllen = sizeof(cmsgbuf.buf); + +while ((n = recvmsg(sock, &msg, 0)) == -1 && errno == EINTR) ; +if (n != 1 || ch != 'A') return -1; + +if (!(cmsg = CMSG_FIRSTHDR(&msg))) return -1; +if (cmsg->cmsg_type != SCM_RIGHTS) return -1; +if ((fd = *(const int *)CMSG_DATA(cmsg)) < 0) return -1; +return fd; +} + + + /************************************************* * Create a log file as the exim user * *************************************************/ @@ -311,41 +387,56 @@ Returns: a file descriptor, or < 0 on failure (errno set) */ int -log_create_as_exim(uschar *name) +log_open_as_exim(uschar * const name) { -pid_t pid = fork(); -int status = 1; int fd = -1; +const uid_t euid = geteuid(); -/* In the subprocess, change uid/gid and do the creation. Return 0 from the -subprocess on success. If we don't check for setuid failures, then the file -can be created as root, so vulnerabilities which cause setuid to fail mean -that the Exim user can use symlinks to cause a file to be opened/created as -root. We always open for append, so can't nuke existing content but it would -still be Rather Bad. */ - -if (pid == 0) +if (euid == exim_uid) + fd = log_open_already_exim(name); +else if (euid == root_uid) { - if (setgid(exim_gid) < 0) - die(US"exim: setgid for log-file creation failed, aborting", - US"Unexpected log failure, please try later"); - if (setuid(exim_uid) < 0) - die(US"exim: setuid for log-file creation failed, aborting", - US"Unexpected log failure, please try later"); - _exit((log_create(name) < 0)? 1 : 0); - } - -/* If we created a subprocess, wait for it. If it succeeded, try the open. */ + int sock[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock) == 0) + { + const pid_t pid = fork(); + if (pid == 0) + { + (void)close(sock[0]); + if ( setgroups(1, &exim_gid) != 0 + || setgid(exim_gid) != 0 + || setuid(exim_uid) != 0 + + || getuid() != exim_uid || geteuid() != exim_uid + || getgid() != exim_gid || getegid() != exim_gid + + || (fd = log_open_already_exim(name)) < 0 + || !log_send_fd(sock[1], fd) + ) _exit(EXIT_FAILURE); + (void)close(sock[1]); + _exit(EXIT_SUCCESS); + } -while (pid > 0 && waitpid(pid, &status, 0) != pid); -if (status == 0) fd = Uopen(name, -#ifdef O_CLOEXEC - O_CLOEXEC | -#endif - O_APPEND|O_WRONLY, LOG_MODE); + (void)close(sock[1]); + if (pid > 0) + { + fd = log_recv_fd(sock[0]); + while (waitpid(pid, NULL, 0) == -1 && errno == EINTR); + } + (void)close(sock[0]); + } + } -/* If we failed to create a subprocess, we are in a bad way. We return -with fd still < 0, and errno set, letting the caller handle the error. */ +if (fd >= 0) + { + int flags; + flags = fcntl(fd, F_GETFD); + if (flags != -1) (void)fcntl(fd, F_SETFD, flags | FD_CLOEXEC); + flags = fcntl(fd, F_GETFL); + if (flags != -1) (void)fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); + } +else + errno = EACCES; return fd; } @@ -396,60 +487,60 @@ people want, I hope. */ ok = string_format(buffer, sizeof(buffer), CS file_path, log_names[type]); -/* Save the name of the mainlog for rollover processing. Without a datestamp, -it gets statted to see if it has been cycled. With a datestamp, the datestamp -will be compared. The static slot for saving it is the same size as buffer, -and the text has been checked above to fit, so this use of strcpy() is OK. */ - -if (type == lt_main && string_datestamp_offset >= 0) +switch (type) { - Ustrcpy(mainlog_name, buffer); - mainlog_datestamp = mainlog_name + string_datestamp_offset; - } - -/* Ditto for the reject log */ - -else if (type == lt_reject && string_datestamp_offset >= 0) - { - Ustrcpy(rejectlog_name, buffer); - rejectlog_datestamp = rejectlog_name + string_datestamp_offset; - } - -/* and deal with the debug log (which keeps the datestamp, but does not -update it) */ + case lt_main: + /* Save the name of the mainlog for rollover processing. Without a datestamp, + it gets statted to see if it has been cycled. With a datestamp, the datestamp + will be compared. The static slot for saving it is the same size as buffer, + and the text has been checked above to fit, so this use of strcpy() is OK. */ + Ustrcpy(mainlog_name, buffer); + if (string_datestamp_offset > 0) + mainlog_datestamp = mainlog_name + string_datestamp_offset; + break; -else if (type == lt_debug) - { - Ustrcpy(debuglog_name, buffer); - if (tag) - { - /* this won't change the offset of the datestamp */ - ok2 = string_format(buffer, sizeof(buffer), "%s%s", - debuglog_name, tag); - if (ok2) - Ustrcpy(debuglog_name, buffer); - } - } + case lt_reject: + /* Ditto for the reject log */ + Ustrcpy(rejectlog_name, buffer); + if (string_datestamp_offset > 0) + rejectlog_datestamp = rejectlog_name + string_datestamp_offset; + break; -/* Remove any datestamp if this is the panic log. This is rare, so there's no -need to optimize getting the datestamp length. We remove one non-alphanumeric -char afterwards if at the start, otherwise one before. */ + case lt_debug: + /* and deal with the debug log (which keeps the datestamp, but does not + update it) */ + Ustrcpy(debuglog_name, buffer); + if (tag) + { + /* this won't change the offset of the datestamp */ + ok2 = string_format(buffer, sizeof(buffer), "%s%s", + debuglog_name, tag); + if (ok2) + Ustrcpy(debuglog_name, buffer); + } + break; -else if (string_datestamp_offset >= 0) - { - uschar * from = buffer + string_datestamp_offset; - uschar * to = from + string_datestamp_length; + default: + /* Remove any datestamp if this is the panic log. This is rare, so there's no + need to optimize getting the datestamp length. We remove one non-alphanumeric + char afterwards if at the start, otherwise one before. */ + if (string_datestamp_offset >= 0) + { + uschar * from = buffer + string_datestamp_offset; + uschar * to = from + string_datestamp_length; - if (from == buffer || from[-1] == '/') - { - if (!isalnum(*to)) to++; - } - else - if (!isalnum(from[-1])) from--; + if (from == buffer || from[-1] == '/') + { + if (!isalnum(*to)) to++; + } + else + if (!isalnum(from[-1])) from--; - /* This copy is ok, because we know that to is a substring of from. But - due to overlap we must use memmove() not Ustrcpy(). */ - memmove(from, to, Ustrlen(to)+1); + /* This copy is ok, because we know that to is a substring of from. But + due to overlap we must use memmove() not Ustrcpy(). */ + memmove(from, to, Ustrlen(to)+1); + } + break; } /* If the file name is too long, it is an unrecoverable disaster */ @@ -458,59 +549,20 @@ if (!ok) die(US"exim: log file path too long: aborting", US"Logging failure; please try later"); -/* We now have the file name. Try to open an existing file. After a successful -open, arrange for automatic closure on exec(), and then return. */ +/* We now have the file name. After a successful open, return. */ -*fd = Uopen(buffer, -#ifdef O_CLOEXEC - O_CLOEXEC | -#endif - O_APPEND|O_WRONLY, LOG_MODE); - -if (*fd >= 0) - { -#ifndef O_CLOEXEC - (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC); -#endif +if ((*fd = log_open_as_exim(buffer)) >= 0) return; - } - -/* Open was not successful: try creating the file. If this is a root process, -we must do the creating in a subprocess set to exim:exim in order to ensure -that the file is created with the right ownership. Otherwise, there can be a -race if another Exim process is trying to write to the log at the same time. -The use of SIGUSR1 by the exiwhat utility can provoke a lot of simultaneous -writing. */ euid = geteuid(); -/* If we are already running as the Exim user (even if that user is root), -we can go ahead and create in the current process. */ - -if (euid == exim_uid) *fd = log_create(buffer); - -/* Otherwise, if we are root, do the creation in an exim:exim subprocess. If we -are neither exim nor root, creation is not attempted. */ - -else if (euid == root_uid) *fd = log_create_as_exim(buffer); - -/* If we now have an open file, set the close-on-exec flag and return. */ - -if (*fd >= 0) - { -#ifndef O_CLOEXEC - (void)fcntl(*fd, F_SETFD, fcntl(*fd, F_GETFD) | FD_CLOEXEC); -#endif - return; - } - /* Creation failed. There are some circumstances in which we get here when the effective uid is not root or exim, which is the problem. (For example, a non-setuid binary with log_arguments set, called in certain ways.) Rather than just bombing out, force the log to stderr and carry on if stderr is available. */ -if (euid != root_uid && euid != exim_uid && log_stderr != NULL) +if (euid != root_uid && euid != exim_uid && log_stderr) { *fd = fileno(log_stderr); return; @@ -519,7 +571,9 @@ if (euid != root_uid && euid != exim_uid && log_stderr != NULL) /* Otherwise this is a disaster. This call is deliberately ONLY to the panic log. If possible, save a copy of the original line that was being logged. If we are recursing (can't open the panic log either), the pointer will already be -set. */ +set. Also, when we had to use a subprocess for the create we didn't retrieve +errno from it, so get the error from the open attempt above (which is often +meaningful enough, so leave it). */ if (!panic_save_buffer) if ((panic_save_buffer = US malloc(LOG_BUFFER_SIZE))) @@ -765,7 +819,7 @@ if (!log_buffer) if (!(log_buffer = US malloc(LOG_BUFFER_SIZE))) { fprintf(stderr, "exim: failed to get store for log buffer\n"); - exim_exit(EXIT_FAILURE, NULL); + exim_exit(EXIT_FAILURE); } /* If we haven't already done so, inspect the setting of log_file_path to @@ -839,6 +893,11 @@ if (!path_inspected) "More than one path given in log_file_path: using %s", file_path); } +/* Optionally trigger debug */ + +if (flags & LOG_PANIC && dtrigger_selector & BIT(DTi_panictrigger)) + debug_trigger_fire(); + /* If debugging, show all log entries, but don't show headers. Do it all in one go so that it doesn't get split when multi-processing. */ @@ -865,9 +924,13 @@ DEBUG(D_any|D_v) if (flags & LOG_CONFIG) g = log_config_info(g, flags); + /* We want to be able to log tainted info, but log_buffer is directly + malloc'd. So use deliberately taint-nonchecking routines to build into + it, trusting that we will never expand the results. */ + va_start(ap, format); i = g->ptr; - if (!string_vformat(g, FALSE, format, ap)) + if (!string_vformat(g, SVFMT_TAINT_NOCHK, format, ap)) { g->ptr = i; g = string_cat(g, US"**** log string overflowed log buffer ****"); @@ -893,6 +956,7 @@ if (!(flags & (LOG_MAIN|LOG_PANIC|LOG_REJECT))) if (f.disable_logging) { DEBUG(D_any) debug_printf("log writing disabled\n"); + if ((flags & LOG_PANIC_DIE) == LOG_PANIC_DIE) exim_exit(EXIT_FAILURE); return; } @@ -921,7 +985,12 @@ if (flags & LOG_CONFIG) va_start(ap, format); { int i = g->ptr; - if (!string_vformat(g, FALSE, format, ap)) + + /* We want to be able to log tainted info, but log_buffer is directly + malloc'd. So use deliberately taint-nonchecking routines to build into + it, trusting that we will never expand the results. */ + + if (!string_vformat(g, SVFMT_TAINT_NOCHK, format, ap)) { g->ptr = i; g = string_cat(g, US"**** log string overflowed log buffer ****\n"); @@ -934,7 +1003,7 @@ this way because it kind of fits with LOG_RECIPIENTS. */ if ( flags & LOG_SENDER && g->ptr < LOG_BUFFER_SIZE - 10 - Ustrlen(raw_sender)) - g = string_fmt_append(g, " from <%s>", raw_sender); + g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " from <%s>", raw_sender); /* Add list of recipients to the message if required; the raw list, before rewriting, was saved in raw_recipients. There may be none, if an ACL @@ -945,12 +1014,12 @@ if ( flags & LOG_RECIPIENTS && raw_recipients_count > 0) { int i; - g = string_fmt_append(g, " for"); + g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " for", NULL); for (i = 0; i < raw_recipients_count; i++) { uschar * s = raw_recipients[i]; if (LOG_BUFFER_SIZE - g->ptr < Ustrlen(s) + 3) break; - g = string_fmt_append(g, " %s", s); + g = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " %s", s); } } @@ -972,7 +1041,7 @@ if (!f.really_exim || f.log_testing_mode) else fprintf(log_stderr, "%s", CS log_buffer); - if ((flags & LOG_PANIC_DIE) == LOG_PANIC_DIE) exim_exit(EXIT_FAILURE, US""); + if ((flags & LOG_PANIC_DIE) == LOG_PANIC_DIE) exim_exit(EXIT_FAILURE); return; } @@ -1046,39 +1115,34 @@ if (flags & LOG_REJECT) { if (header_list && LOGGING(rejected_header)) { - uschar * p = g->s + g->ptr; + gstring * g2; int i; if (recipients_count > 0) { /* List the sender */ - string_format(p, LOG_BUFFER_SIZE - g->ptr, - "Envelope-from: <%s>\n", sender_address); - while (*p) p++; - g->ptr = p - g->s; + g2 = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, + "Envelope-from: <%s>\n", sender_address); + if (g2) g = g2; /* List up to 5 recipients */ - string_format(p, LOG_BUFFER_SIZE - g->ptr, - "Envelope-to: <%s>\n", recipients_list[0].address); - while (*p) p++; - g->ptr = p - g->s; + g2 = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, + "Envelope-to: <%s>\n", recipients_list[0].address); + if (g2) g = g2; for (i = 1; i < recipients_count && i < 5; i++) { - string_format(p, LOG_BUFFER_SIZE - g->ptr, " <%s>\n", - recipients_list[i].address); - while (*p) p++; - g->ptr = p - g->s; + g2 = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, + " <%s>\n", recipients_list[i].address); + if (g2) g = g2; } if (i < recipients_count) { - string_format(p, LOG_BUFFER_SIZE - g->ptr, - " ...\n"); - while (*p) p++; - g->ptr = p - g->s; + g2 = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, " ...\n", NULL); + if (g2) g = g2; } } @@ -1086,11 +1150,11 @@ if (flags & LOG_REJECT) for (header_line * h = header_list; h; h = h->next) if (h->text) { - BOOL fitted = string_format(p, LOG_BUFFER_SIZE - g->ptr, - "%c %s", h->type, h->text); - while (*p) p++; - g->ptr = p - g->s; - if (!fitted) /* Buffer is full; truncate */ + g2 = string_fmt_append_f(g, SVFMT_TAINT_NOCHK, + "%c %s", h->type, h->text); + if (g2) + g = g2; + else /* Buffer is full; truncate */ { g->ptr -= 100; /* For message and separator */ if (g->s[g->ptr-1] == '\n') g->ptr--; @@ -1181,10 +1245,7 @@ if (flags & LOG_PANIC) panic_recurseflag = FALSE; if (panic_save_buffer) - { - int i = write(paniclogfd, panic_save_buffer, Ustrlen(panic_save_buffer)); - i = i; /* compiler quietening */ - } + (void) write(paniclogfd, panic_save_buffer, Ustrlen(panic_save_buffer)); written_len = write_to_fd_buf(paniclogfd, g->s, g->ptr); if (written_len != g->ptr) @@ -1292,14 +1353,14 @@ decode_bits(unsigned int *selector, size_t selsize, int *notall, uschar *string, bit_table *options, int count, uschar *which, int flags) { uschar *errmsg; -if (string == NULL) return; +if (!string) return; if (*string == '=') { char *end; /* Not uschar */ memset(selector, 0, sizeof(*selector)*selsize); *selector = strtoul(CS string+1, &end, 0); - if (*end == 0) return; + if (!*end) return; errmsg = string_sprintf("malformed numeric %s_selector setting: %s", which, string); goto ERROR_RETURN; @@ -1314,8 +1375,8 @@ else for(;;) int len; bit_table *start, *end; - while (isspace(*string)) string++; - if (*string == 0) return; + Uskip_whitespace(&string); + if (!*string) return; if (*string != '+' && *string != '-') { @@ -1337,7 +1398,6 @@ else for(;;) bit_table *middle = start + (end - start)/2; int c = Ustrncmp(s, middle->name, len); if (c == 0) - { if (middle->name[len] != 0) c = -1; else { unsigned int bit = middle->bit; @@ -1359,7 +1419,6 @@ else for(;;) break; /* Out of loop to match selector name */ } - } if (c < 0) end = middle; else start = middle + 1; } /* Loop to match selector name */ @@ -1403,13 +1462,15 @@ misconfiguration. The first use of this is in ACL logic, "control = debug/tag=foo/opts=+expand" which can be combined with conditions, etc, to activate extra logging only -for certain sources. The second use is inetd wait mode debug preservation. */ +for certain sources. The second use is inetd wait mode debug preservation. + +It might be nice, in ACL-initiated pretrigger mode, to not create the file +immediately but only upon a trigger - but we'd need another cmdline option +to pass the name through child_exxec_exim(). */ void debug_logging_activate(uschar *tag_name, uschar *opts) { -int fd = -1; - if (debug_file) { debug_printf("DEBUGGING ACTIVATED FROM WITHIN CONFIG.\n" @@ -1417,7 +1478,7 @@ if (debug_file) return; } -if (tag_name != NULL && (Ustrchr(tag_name, '/') != NULL)) +if (tag_name && (Ustrchr(tag_name, '/') != NULL)) { log_write(0, LOG_MAIN|LOG_PANIC, "debug tag may not contain a '/' in: %s", tag_name); @@ -1435,24 +1496,26 @@ do not segfault; note that nondefault log locations will not work */ if (!*file_path) set_file_path(); -open_log(&fd, lt_debug, tag_name); +open_log(&debug_fd, lt_debug, tag_name); -if (fd != -1) - debug_file = fdopen(fd, "w"); +if (debug_fd != -1) + debug_file = fdopen(debug_fd, "w"); else log_write(0, LOG_MAIN|LOG_PANIC, "unable to open debug log"); } void -debug_logging_stop(void) +debug_logging_stop(BOOL kill) { +debug_pretrigger_discard(); if (!debug_file || !debuglog_name[0]) return; debug_selector = 0; fclose(debug_file); debug_file = NULL; -unlink_log(lt_debug); +debug_fd = -1; +if (kill) unlink_log(lt_debug); }