designated initializers
[exim.git] / src / src / log.c
index bb6902ea082af8b86d7c72494c1f136655e057bf..0a41cd35adaf50a824a78fffee972aa7ee982ba7 100644 (file)
@@ -3,7 +3,7 @@
 *************************************************/
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
-/* Copyright (c) The Exim Maintainers 2020 */
+/* 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
@@ -55,66 +55,68 @@ static size_t pid_position[2];
 number definitions in macros.h */
 
 static const uschar * exim_errstrings[] = {
-  US"",
-  US"unknown error",
-  US"user slash",
-  US"exist race",
-  US"not regular",
-  US"not directory",
-  US"bad ugid",
-  US"bad mode",
-  US"inode changed",
-  US"lock failed",
-  US"bad address2",
-  US"forbid pipe",
-  US"forbid file",
-  US"forbid reply",
-  US"missing pipe",
-  US"missing file",
-  US"missing reply",
-  US"bad redirect",
-  US"smtp closed",
-  US"smtp format",
-  US"spool format",
-  US"not absolute",
-  US"Exim-imposed quota",
-  US"held",
-  US"Delivery filter process failure",
-  US"Delivery add/remove header failure",
-  US"Delivery write incomplete error",
-  US"Some expansion failed",
-  US"Failed to get gid",
-  US"Failed to get uid",
-  US"Unset or non-existent transport",
-  US"MBX length mismatch",
-  US"Lookup failed routing or in smtp tpt",
-  US"Can't match format in appendfile",
-  US"Creation outside home in appendfile",
-  US"Can't check a list; lookup defer",
-  US"DNS lookup defer",
-  US"Failed to start TLS session",
-  US"Mandatory TLS session not started",
-  US"Failed to chown a file",
-  US"Failed to create a pipe",
-  US"When verifying",
-  US"When required by client",
-  US"Used internally in smtp transport",
-  US"RCPT gave 4xx error",
-  US"MAIL gave 4xx error",
-  US"DATA gave 4xx error",
-  US"Negotiation failed for proxy configured host",
-  US"Authenticator 'other' failure",
-  US"target not supporting SMTPUTF8",
-  US"host is local",
-  US"tainted filename",
-
-  US"Not time for routing",
-  US"Not time for local delivery",
-  US"Not time for any remote host",
-  US"Local-only delivery",
-  US"Domain in queue_domains",
-  US"Transport concurrency limit",
-  US"Event requests alternate response",
+  [0] = US"",
+  [- ERRNO_UNKNOWNERROR] =     US"unknown error",
+  [- ERRNO_USERSLASH] =                US"user slash",
+  [- ERRNO_EXISTRACE] =                US"exist race",
+  [- ERRNO_NOTREGULAR] =       US"not regular",
+  [- ERRNO_NOTDIRECTORY] =     US"not directory",
+  [- ERRNO_BADUGID] =          US"bad ugid",
+  [- ERRNO_BADMODE] =          US"bad mode",
+  [- ERRNO_INODECHANGED] =     US"inode changed",
+  [- ERRNO_LOCKFAILED] =       US"lock failed",
+  [- ERRNO_BADADDRESS2] =      US"bad address2",
+  [- ERRNO_FORBIDPIPE] =       US"forbid pipe",
+  [- ERRNO_FORBIDFILE] =       US"forbid file",
+  [- ERRNO_FORBIDREPLY] =      US"forbid reply",
+  [- ERRNO_MISSINGPIPE] =      US"missing pipe",
+  [- ERRNO_MISSINGFILE] =      US"missing file",
+  [- ERRNO_MISSINGREPLY] =     US"missing reply",
+  [- ERRNO_BADREDIRECT] =      US"bad redirect",
+  [- ERRNO_SMTPCLOSED] =       US"smtp closed",
+  [- ERRNO_SMTPFORMAT] =       US"smtp format",
+  [- ERRNO_SPOOLFORMAT] =      US"spool format",
+  [- ERRNO_NOTABSOLUTE] =      US"not absolute",
+  [- ERRNO_EXIMQUOTA] =                US"Exim-imposed quota",
+  [- ERRNO_HELD] =             US"held",
+  [- ERRNO_FILTER_FAIL] =      US"Delivery filter process failure",
+  [- ERRNO_CHHEADER_FAIL] =    US"Delivery add/remove header failure",
+  [- ERRNO_WRITEINCOMPLETE] =  US"Delivery write incomplete error",
+  [- ERRNO_EXPANDFAIL] =       US"Some expansion failed",
+  [- ERRNO_GIDFAIL] =          US"Failed to get gid",
+  [- ERRNO_UIDFAIL] =          US"Failed to get uid",
+  [- ERRNO_BADTRANSPORT] =     US"Unset or non-existent transport",
+  [- ERRNO_MBXLENGTH] =                US"MBX length mismatch",
+  [- ERRNO_UNKNOWNHOST] =      US"Lookup failed routing or in smtp tpt",
+  [- ERRNO_FORMATUNKNOWN] =    US"Can't match format in appendfile",
+  [- ERRNO_BADCREATE] =                US"Creation outside home in appendfile",
+  [- ERRNO_LISTDEFER] =                US"Can't check a list; lookup defer",
+  [- ERRNO_DNSDEFER] =         US"DNS lookup defer",
+  [- ERRNO_TLSFAILURE] =       US"Failed to start TLS session",
+  [- ERRNO_TLSREQUIRED] =      US"Mandatory TLS session not started",
+  [- ERRNO_CHOWNFAIL] =                US"Failed to chown a file",
+  [- ERRNO_PIPEFAIL] =         US"Failed to create a pipe",
+  [- ERRNO_CALLOUTDEFER] =     US"When verifying",
+  [- ERRNO_AUTHFAIL] =         US"When required by client",
+  [- ERRNO_CONNECTTIMEOUT] =   US"Used internally in smtp transport",
+  [- ERRNO_RCPT4XX] =          US"RCPT gave 4xx error",
+  [- ERRNO_MAIL4XX] =          US"MAIL gave 4xx error",
+  [- ERRNO_DATA4XX] =          US"DATA gave 4xx error",
+  [- ERRNO_PROXYFAIL] =                US"Negotiation failed for proxy configured host",
+  [- ERRNO_AUTHPROB] =         US"Authenticator 'other' failure",
+  [- ERRNO_UTF8_FWD] =         US"target not supporting SMTPUTF8",
+  [- ERRNO_HOST_IS_LOCAL] =    US"host is local",
+  [- ERRNO_TAINT] =            US"tainted filename",
+
+  [- ERRNO_RRETRY] =           US"Not time for routing",
+
+  [- ERRNO_LRETRY] =           US"Not time for local delivery",
+  [- ERRNO_HRETRY] =           US"Not time for any remote host",
+  [- ERRNO_LOCAL_ONLY] =       US"Local-only delivery",
+  [- ERRNO_QUEUE_DOMAIN] =     US"Domain in queue_domains",
+  [- ERRNO_TRETRY] =           US"Transport concurrency limit",
+
+  [- ERRNO_EVENT] =            US"Event requests alternate response",
 };
 
 
@@ -288,8 +290,11 @@ if (fd < 0 && errno == ENOENT)
   uschar *lastslash = Ustrrchr(name, '/');
   *lastslash = 0;
   created = directory_make(NULL, name, LOG_DIRECTORY_MODE, FALSE);
-  DEBUG(D_any) debug_printf("%s log directory %s\n",
-    created ? "created" : "failed to create", name);
+  DEBUG(D_any)
+    if (created)
+      debug_printf("created log directory %s\n", name);
+    else
+      debug_printf("failed to create log directory %s: %s\n", name, strerror(errno));
   *lastslash = '/';
   if (created) fd = Uopen(name, flags, LOG_MODE);
   }
@@ -299,9 +304,12 @@ return fd;
 
 
 
-/* Inspired by OpenSSH's mm_send_fd(). Thanks! */
+/* Inspired by OpenSSH's mm_send_fd(). Thanks!
+Send fd over socketpair.
+Return: true iff good.
+*/
 
-static int
+static BOOL
 log_send_fd(const int sock, const int fd)
 {
 struct msghdr msg;
@@ -310,8 +318,8 @@ union {
   char buf[CMSG_SPACE(sizeof(int))];
 } cmsgbuf;
 struct cmsghdr *cmsg;
-struct iovec vec;
 char ch = 'A';
+struct iovec vec = {.iov_base = &ch, .iov_len = 1};
 ssize_t n;
 
 memset(&msg, 0, sizeof(msg));
@@ -325,17 +333,16 @@ cmsg->cmsg_level = SOL_SOCKET;
 cmsg->cmsg_type = SCM_RIGHTS;
 *(int *)CMSG_DATA(cmsg) = fd;
 
-vec.iov_base = &ch;
-vec.iov_len = 1;
 msg.msg_iov = &vec;
 msg.msg_iovlen = 1;
 
 while ((n = sendmsg(sock, &msg, 0)) == -1 && errno == EINTR);
-if (n != 1) return -1;
-return 0;
+return n == 1;
 }
 
-/* Inspired by OpenSSH's mm_receive_fd(). Thanks! */
+/* 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)
@@ -346,14 +353,12 @@ union {
   char buf[CMSG_SPACE(sizeof(int))];
 } cmsgbuf;
 struct cmsghdr *cmsg;
-struct iovec vec;
-ssize_t n;
 char ch = '\0';
-int fd = -1;
+struct iovec vec = {.iov_base = &ch, .iov_len = 1};
+ssize_t n;
+int fd;
 
 memset(&msg, 0, sizeof(msg));
-vec.iov_base = &ch;
-vec.iov_len = 1;
 msg.msg_iov = &vec;
 msg.msg_iovlen = 1;
 
@@ -361,14 +366,12 @@ 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);
+while ((n = recvmsg(sock, &msg, 0)) == -1 && errno == EINTR) ;
 if (n != 1 || ch != 'A') return -1;
 
-cmsg = CMSG_FIRSTHDR(&msg);
-if (cmsg == NULL) return -1;
+if (!(cmsg = CMSG_FIRSTHDR(&msg))) return -1;
 if (cmsg->cmsg_type != SCM_RIGHTS) return -1;
-fd = *(const int *)CMSG_DATA(cmsg);
-if (fd < 0) return -1;
+if ((fd = *(const int *)CMSG_DATA(cmsg)) < 0) return -1;
 return fd;
 }
 
@@ -395,28 +398,26 @@ int fd = -1;
 const uid_t euid = geteuid();
 
 if (euid == exim_uid)
-  {
   fd = log_open_already_exim(name);
-  }
 else if (euid == root_uid)
   {
   int sock[2];
   if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock) == 0)
     {
-    const pid_t pid = exim_fork(US"logfile-open");
+    const pid_t pid = fork();
     if (pid == 0)
       {
       (void)close(sock[0]);
-      if (setgroups(1, &exim_gid) != 0) _exit(EXIT_FAILURE);
-      if (setgid(exim_gid) != 0) _exit(EXIT_FAILURE);
-      if (setuid(exim_uid) != 0) _exit(EXIT_FAILURE);
+      if (  setgroups(1, &exim_gid) != 0
+         || setgid(exim_gid) != 0
+         || setuid(exim_uid) != 0
 
-      if (getuid() != exim_uid || geteuid() != exim_uid) _exit(EXIT_FAILURE);
-      if (getgid() != exim_gid || getegid() != exim_gid) _exit(EXIT_FAILURE);
+         || getuid() != exim_uid || geteuid() != exim_uid
+         || getgid() != exim_gid || getegid() != exim_gid
 
-      fd = log_open_already_exim(name);
-      if (fd < 0) _exit(EXIT_FAILURE);
-      if (log_send_fd(sock[1], fd) != 0) _exit(EXIT_FAILURE);
+         || (fd = log_open_already_exim(name)) < 0
+         || !log_send_fd(sock[1], fd)
+        ) _exit(EXIT_FAILURE);
       (void)close(sock[1]);
       _exit(EXIT_SUCCESS);
       }
@@ -440,9 +441,7 @@ if (fd >= 0)
   if (flags != -1) (void)fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
   }
 else
-  {
   errno = EACCES;
-  }
 
 return fd;
 }
@@ -458,7 +457,7 @@ return fd;
 it does not exist. This may be called recursively on failure, in order to open
 the panic log.
 
-The directory is in the static variable file_path. This is static so that it
+The directory is in the static variable file_path. This is static so that
 the work of sorting out the path is done just once per Exim process.
 
 Exim is normally configured to avoid running as root wherever possible, the log
@@ -493,62 +492,64 @@ 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)
-  {
-  Ustrcpy(mainlog_name, buffer);
-  if (string_datestamp_offset > 0)
-    mainlog_datestamp = mainlog_name + string_datestamp_offset;
-  }
-
-/* Ditto for the reject log */
-
-else if (type == lt_reject)
+switch (type)
   {
-  Ustrcpy(rejectlog_name, buffer);
-  if (string_datestamp_offset > 0)
-    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)
+      {
+      if (is_tainted(tag))
+       die(US"exim: tainted tag for debug log filename",
+             US"Logging failure; please try later");
+
+      /* 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 */
@@ -559,12 +560,8 @@ if (!ok)
 
 /* We now have the file name. After a successful open, return. */
 
-*fd = log_open_as_exim(buffer);
-
-if (*fd >= 0)
-  {
+if ((*fd = log_open_as_exim(buffer)) >= 0)
   return;
-  }
 
 euid = geteuid();
 
@@ -732,10 +729,17 @@ while ((t = string_nextinlist(&tt, &sep, log_buffer, LOG_BUFFER_SIZE)))
 }
 
 
+/* Close mainlog, unless we do not see a chance to open the file mainlog later
+again.  This will happen if we log from a transport process (which has dropped
+privs); something we traditionally avoid, but the introduction of taint-tracking
+and resulting detection of errors is makinng harder. */
+
 void
 mainlog_close(void)
 {
-if (mainlogfd < 0) return;
+if (mainlogfd < 0
+   || !(geteuid() == 0 || geteuid() == exim_uid))
+  return;
 (void)close(mainlogfd);
 mainlogfd = -1;
 mainlog_inode = 0;
@@ -905,6 +909,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. */
 
@@ -1469,13 +1478,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"
@@ -1483,7 +1494,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);
@@ -1501,24 +1512,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);
 }