Merge branch 'list_safety'
[exim.git] / src / src / exim.c
index 6b82013f879979c4529d4cd57797e3eaf417b71a..670319b989db251032d98d02ae12564e4d25eff5 100644 (file)
@@ -1,5 +1,3 @@
-/* $Cambridge: exim/src/src/exim.c,v 1.71 2010/06/07 00:12:42 pdp Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
@@ -14,6 +12,8 @@ Also a few functions that don't naturally fit elsewhere. */
 
 #include "exim.h"
 
+extern void init_lookup_list(void);
+
 
 
 /*************************************************
@@ -138,6 +138,38 @@ return yield;
 
 
 
+/*************************************************
+*            Set up processing details           *
+*************************************************/
+
+/* Save a text string for dumping when SIGUSR1 is received.
+Do checks for overruns.
+
+Arguments: format and arguments, as for printf()
+Returns:   nothing
+*/
+
+void
+set_process_info(const char *format, ...)
+{
+int len;
+va_list ap;
+sprintf(CS process_info, "%5d ", (int)getpid());
+len = Ustrlen(process_info);
+va_start(ap, format);
+if (!string_vformat(process_info + len, PROCESS_INFO_SIZE - len - 2, format, ap))
+  Ustrcpy(process_info + len, "**** string overflowed buffer ****");
+len = Ustrlen(process_info);
+process_info[len+0] = '\n';
+process_info[len+1] = '\0';
+process_info_len = len + 1;
+DEBUG(D_process_info) debug_printf("set_process_info: %s", process_info);
+va_end(ap);
+}
+
+
+
+
 /*************************************************
 *             Handler for SIGUSR1                *
 *************************************************/
@@ -147,6 +179,8 @@ what it is currently doing. It will only be used if the OS is capable of
 setting up a handler that causes automatic restarting of any system call
 that is in progress at the time.
 
+This function takes care to be signal-safe.
+
 Argument: the signal number (SIGUSR1)
 Returns:  nothing
 */
@@ -154,10 +188,32 @@ Returns:  nothing
 static void
 usr1_handler(int sig)
 {
-sig = sig;      /* Keep picky compilers happy */
-log_write(0, LOG_PROCESS, "%s", process_info);
-log_close_all();
-os_restarting_signal(SIGUSR1, usr1_handler);
+int fd;
+
+os_restarting_signal(sig, usr1_handler);
+
+fd = Uopen(process_log_path, O_APPEND|O_WRONLY, LOG_MODE);
+if (fd < 0)
+  {
+  /* If we are already running as the Exim user, try to create it in the
+  current process (assuming spool_directory exists). Otherwise, if we are
+  root, do the creation in an exim:exim subprocess. */
+
+  int euid = geteuid();
+  if (euid == exim_uid)
+    fd = Uopen(process_log_path, O_CREAT|O_APPEND|O_WRONLY, LOG_MODE);
+  else if (euid == root_uid)
+    fd = log_create_as_exim(process_log_path);
+  }
+
+/* If we are neither exim nor root, or if we failed to create the log file,
+give up. There is not much useful we can do with errors, since we don't want
+to disrupt whatever is going on outside the signal handler. */
+
+if (fd < 0) return;
+
+(void)write(fd, process_info, process_info_len);
+(void)close(fd);
 }
 
 
@@ -346,35 +402,6 @@ if (exim_tvcmp(&now_tv, then_tv) <= 0)
 
 
 
-/*************************************************
-*            Set up processing details           *
-*************************************************/
-
-/* Save a text string for dumping when SIGUSR1 is received.
-Do checks for overruns.
-
-Arguments: format and arguments, as for printf()
-Returns:   nothing
-*/
-
-void
-set_process_info(char *format, ...)
-{
-int len;
-va_list ap;
-sprintf(CS process_info, "%5d ", (int)getpid());
-len = Ustrlen(process_info);
-va_start(ap, format);
-if (!string_vformat(process_info + len, PROCESS_INFO_SIZE - len, format, ap))
-  Ustrcpy(process_info + len, "**** string overflowed buffer ****");
-DEBUG(D_process_info) debug_printf("set_process_info: %s\n", process_info);
-va_end(ap);
-}
-
-
-
-
-
 /*************************************************
 *   Call fopen() with umask 777 and adjust mode  *
 *************************************************/
@@ -395,7 +422,7 @@ Returns:          the fopened FILE or NULL
 */
 
 FILE *
-modefopen(uschar *filename, char *options, mode_t mode)
+modefopen(const uschar *filename, const char *options, mode_t mode)
 {
 mode_t saved_umask = umask(0777);
 FILE *f = Ufopen(filename, options);
@@ -568,17 +595,20 @@ if (euid == root_uid || euid != uid || egid != gid || igflag)
 
 DEBUG(D_uid)
   {
-  int group_count;
+  int group_count, save_errno;
   gid_t group_list[NGROUPS_MAX];
   debug_printf("changed uid/gid: %s\n  uid=%ld gid=%ld pid=%ld\n", msg,
     (long int)geteuid(), (long int)getegid(), (long int)getpid());
   group_count = getgroups(NGROUPS_MAX, group_list);
+  save_errno = errno;
   debug_printf("  auxiliary group list:");
   if (group_count > 0)
     {
     int i;
     for (i = 0; i < group_count; i++) debug_printf(" %d", (int)group_list[i]);
     }
+  else if (group_count < 0)
+    debug_printf(" <error: %s>", strerror(save_errno));
   else debug_printf(" <none>");
   debug_printf("\n");
   }
@@ -776,53 +806,53 @@ fprintf(f, "Support for:");
 #endif
 fprintf(f, "\n");
 
-fprintf(f, "Lookups:");
-#ifdef LOOKUP_LSEARCH
+fprintf(f, "Lookups (built-in):");
+#if defined(LOOKUP_LSEARCH) && LOOKUP_LSEARCH!=2
   fprintf(f, " lsearch wildlsearch nwildlsearch iplsearch");
 #endif
-#ifdef LOOKUP_CDB
+#if defined(LOOKUP_CDB) && LOOKUP_CDB!=2
   fprintf(f, " cdb");
 #endif
-#ifdef LOOKUP_DBM
+#if defined(LOOKUP_DBM) && LOOKUP_DBM!=2
   fprintf(f, " dbm dbmnz");
 #endif
-#ifdef LOOKUP_DNSDB
+#if defined(LOOKUP_DNSDB) && LOOKUP_DNSDB!=2
   fprintf(f, " dnsdb");
 #endif
-#ifdef LOOKUP_DSEARCH
+#if defined(LOOKUP_DSEARCH) && LOOKUP_DSEARCH!=2
   fprintf(f, " dsearch");
 #endif
-#ifdef LOOKUP_IBASE
+#if defined(LOOKUP_IBASE) && LOOKUP_IBASE!=2
   fprintf(f, " ibase");
 #endif
-#ifdef LOOKUP_LDAP
+#if defined(LOOKUP_LDAP) && LOOKUP_LDAP!=2
   fprintf(f, " ldap ldapdn ldapm");
 #endif
-#ifdef LOOKUP_MYSQL
+#if defined(LOOKUP_MYSQL) && LOOKUP_MYSQL!=2
   fprintf(f, " mysql");
 #endif
-#ifdef LOOKUP_NIS
+#if defined(LOOKUP_NIS) && LOOKUP_NIS!=2
   fprintf(f, " nis nis0");
 #endif
-#ifdef LOOKUP_NISPLUS
+#if defined(LOOKUP_NISPLUS) && LOOKUP_NISPLUS!=2
   fprintf(f, " nisplus");
 #endif
-#ifdef LOOKUP_ORACLE
+#if defined(LOOKUP_ORACLE) && LOOKUP_ORACLE!=2
   fprintf(f, " oracle");
 #endif
-#ifdef LOOKUP_PASSWD
+#if defined(LOOKUP_PASSWD) && LOOKUP_PASSWD!=2
   fprintf(f, " passwd");
 #endif
-#ifdef LOOKUP_PGSQL
+#if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2
   fprintf(f, " pgsql");
 #endif
-#ifdef LOOKUP_SQLITE
+#if defined(LOOKUP_SQLITE) && LOOKUP_SQLITE!=2
   fprintf(f, " sqlite");
 #endif
-#ifdef LOOKUP_TESTDB
+#if defined(LOOKUP_TESTDB) && LOOKUP_TESTDB!=2
   fprintf(f, " testdb");
 #endif
-#ifdef LOOKUP_WHOSON
+#if defined(LOOKUP_WHOSON) && LOOKUP_WHOSON!=2
   fprintf(f, " whoson");
 #endif
 fprintf(f, "\n");
@@ -907,13 +937,67 @@ if (fixed_never_users[0] > 0)
 
 fprintf(f, "Size of off_t: " SIZE_T_FMT "\n", sizeof(off_t));
 
-/* This runtime check is to help diagnose library linkage mismatches which
-result in segfaults and the like; as such, it's left until the end,
-just in case.  There will still be a "Configuration file is" line still to
-come. */
+/* Everything else is details which are only worth reporting when debugging.
+Perhaps the tls_version_report should move into this too. */
+DEBUG(D_any) do {
+
+  int i;
+
+/* clang defines __GNUC__ (at least, for me) so test for it first */
+#if defined(__clang__)
+  fprintf(f, "Compiler: CLang [%s]\n", __clang_version__);
+#elif defined(__GNUC__)
+  fprintf(f, "Compiler: GCC [%s]\n",
+# ifdef __VERSION__
+      __VERSION__
+# else
+      "? unknown version ?"
+# endif
+      );
+#else
+  fprintf(f, "Compiler: <unknown>\n");
+#endif
+
 #ifdef SUPPORT_TLS
-tls_version_report(f);
+  tls_version_report(f);
+#endif
+
+#ifdef AUTH_CYRUS_SASL
+  auth_cyrus_sasl_version_report(f);
+#endif
+
+  fprintf(f, "Library version: PCRE: Compile: %d.%d%s\n"
+             "                       Runtime: %s\n",
+          PCRE_MAJOR, PCRE_MINOR,
+          /* PRE_PRERELEASE is either defined and empty or a string.
+           * unless its an ancient version of PCRE in which case it
+           * is not defined */
+#ifdef PCRE_PRERELEASE
+          PCRE_PRERELEASE "",
+#else
+          "",
 #endif
+          pcre_version());
+
+  init_lookup_list();
+  for (i = 0; i < lookup_list_count; i++)
+    {
+    if (lookup_list[i]->version_report)
+      lookup_list[i]->version_report(f);
+    }
+
+#ifdef WHITELIST_D_MACROS
+  fprintf(f, "WHITELIST_D_MACROS: \"%s\"\n", WHITELIST_D_MACROS);
+#else
+  fprintf(f, "WHITELIST_D_MACROS unset\n");
+#endif
+#ifdef TRUSTED_CONFIG_LIST
+  fprintf(f, "TRUSTED_CONFIG_LIST: \"%s\"\n", TRUSTED_CONFIG_LIST);
+#else
+  fprintf(f, "TRUSTED_CONFIG_LIST unset\n");
+#endif
+
+} while (0);
 }
 
 
@@ -990,19 +1074,23 @@ Returns:            the dlopen handle or NULL on failure
 */
 
 static void *
-set_readline(char * (**fn_readline_ptr)(char *),
-             char * (**fn_addhist_ptr)(char *))
+set_readline(char * (**fn_readline_ptr)(const char *),
+             void   (**fn_addhist_ptr)(const char *))
 {
 void *dlhandle;
-void *dlhandle_curses = dlopen("libcurses.so", RTLD_GLOBAL|RTLD_LAZY);
+void *dlhandle_curses = dlopen("libcurses." DYNLIB_FN_EXT, RTLD_GLOBAL|RTLD_LAZY);
 
-dlhandle = dlopen("libreadline.so", RTLD_GLOBAL|RTLD_NOW);
+dlhandle = dlopen("libreadline." DYNLIB_FN_EXT, RTLD_GLOBAL|RTLD_NOW);
 if (dlhandle_curses != NULL) dlclose(dlhandle_curses);
 
 if (dlhandle != NULL)
   {
-  *fn_readline_ptr = (char *(*)(char*))dlsym(dlhandle, "readline");
-  *fn_addhist_ptr = (char *(*)(char*))dlsym(dlhandle, "add_history");
+  /* Checked manual pages; at least in GNU Readline 6.1, the prototypes are:
+   *   char * readline (const char *prompt);
+   *   void add_history (const char *string);
+   */
+  *fn_readline_ptr = (char *(*)(const char*))dlsym(dlhandle, "readline");
+  *fn_addhist_ptr = (void(*)(const char*))dlsym(dlhandle, "add_history");
   }
 else
   {
@@ -1032,7 +1120,7 @@ Returns:        pointer to dynamic memory, or NULL at end of file
 */
 
 static uschar *
-get_stdinput(char *(*fn_readline)(char *), char *(*fn_addhist)(char *))
+get_stdinput(char *(*fn_readline)(const char *), void(*fn_addhist)(const char *))
 {
 int i;
 int size = 0;
@@ -1131,6 +1219,117 @@ exit(EXIT_FAILURE);
 
 
 
+/*************************************************
+*    Validate that the macros given are okay     *
+*************************************************/
+
+/* Typically, Exim will drop privileges if macros are supplied.  In some
+cases, we want to not do so.
+
+Arguments:    none (macros is a global)
+Returns:      true if trusted, false otherwise
+*/
+
+static BOOL
+macros_trusted(void)
+{
+#ifdef WHITELIST_D_MACROS
+macro_item *m;
+uschar *whitelisted, *end, *p, **whites, **w;
+int white_count, i, n;
+size_t len;
+BOOL prev_char_item, found;
+#endif
+
+if (macros == NULL)
+  return TRUE;
+#ifndef WHITELIST_D_MACROS
+return FALSE;
+#else
+
+/* We only trust -D overrides for some invoking users:
+root, the exim run-time user, the optional config owner user.
+I don't know why config-owner would be needed, but since they can own the
+config files anyway, there's no security risk to letting them override -D. */
+if ( ! ((real_uid == root_uid)
+     || (real_uid == exim_uid)
+#ifdef CONFIGURE_OWNER
+     || (real_uid == config_uid)
+#endif
+   ))
+  {
+  debug_printf("macros_trusted rejecting macros for uid %d\n", (int) real_uid);
+  return FALSE;
+  }
+
+/* Get a list of macros which are whitelisted */
+whitelisted = string_copy_malloc(US WHITELIST_D_MACROS);
+prev_char_item = FALSE;
+white_count = 0;
+for (p = whitelisted; *p != '\0'; ++p)
+  {
+  if (*p == ':' || isspace(*p))
+    {
+    *p = '\0';
+    if (prev_char_item)
+      ++white_count;
+    prev_char_item = FALSE;
+    continue;
+    }
+  if (!prev_char_item)
+    prev_char_item = TRUE;
+  }
+end = p;
+if (prev_char_item)
+  ++white_count;
+if (!white_count)
+  return FALSE;
+whites = store_malloc(sizeof(uschar *) * (white_count+1));
+for (p = whitelisted, i = 0; (p != end) && (i < white_count); ++p)
+  {
+  if (*p != '\0')
+    {
+    whites[i++] = p;
+    if (i == white_count)
+      break;
+    while (*p != '\0' && p < end)
+      ++p;
+    }
+  }
+whites[i] = NULL;
+
+/* The list of macros should be very short.  Accept the N*M complexity. */
+for (m = macros; m != NULL; m = m->next)
+  {
+  found = FALSE;
+  for (w = whites; *w; ++w)
+    if (Ustrcmp(*w, m->name) == 0)
+      {
+      found = TRUE;
+      break;
+      }
+  if (!found)
+    return FALSE;
+  if (m->replacement == NULL)
+    continue;
+  len = Ustrlen(m->replacement);
+  if (len == 0)
+    continue;
+  n = pcre_exec(regex_whitelisted_macro, NULL, CS m->replacement, len,
+   0, PCRE_EOPT, NULL, 0);
+  if (n < 0)
+    {
+    if (n != PCRE_ERROR_NOMATCH)
+      debug_printf("macros_trusted checking %s returned %d\n", m->name, n);
+    return FALSE;
+    }
+  }
+DEBUG(D_any) debug_printf("macros_trusted overridden to true by whitelisting\n");
+return TRUE;
+#endif
+}
+
+
 /*************************************************
 *          Entry point and high-level code       *
 *************************************************/
@@ -1160,7 +1359,7 @@ int  arg_error_handling = error_handling;
 int  filter_sfd = -1;
 int  filter_ufd = -1;
 int  group_count;
-int  i;
+int  i, rv;
 int  list_queue_option = 0;
 int  msg_action = 0;
 int  msg_action_arg = -1;
@@ -1240,7 +1439,19 @@ if (route_finduser(US EXIM_USERNAME, &pw, &exim_uid))
       EXIM_USERNAME);
     exit(EXIT_FAILURE);
     }
-  exim_gid = pw->pw_gid;
+  /* If ref:name uses a number as the name, route_finduser() returns
+  TRUE with exim_uid set and pw coerced to NULL. */
+  if (pw)
+    exim_gid = pw->pw_gid;
+#ifndef EXIM_GROUPNAME
+  else
+    {
+    fprintf(stderr,
+        "exim: ref:name should specify a usercode, not a group.\n"
+        "exim: can't let you get away with it unless you also specify a group.\n");
+    exit(EXIT_FAILURE);
+    }
+#endif
   }
 else
   {
@@ -1268,6 +1479,10 @@ if (!route_finduser(US CONFIGURE_OWNERNAME, NULL, &config_uid))
   }
 #endif
 
+/* We default the system_filter_user to be the Exim run-time user, as a
+sane non-root value. */
+system_filter_uid = exim_uid;
+
 #ifdef CONFIGURE_GROUPNAME
 if (!route_findgroup(US CONFIGURE_GROUPNAME, &config_gid))
   {
@@ -1413,6 +1628,15 @@ regex_smtp_code =
   regex_must_compile(US"^\\d\\d\\d\\s(?:\\d\\.\\d\\d?\\d?\\.\\d\\d?\\d?\\s)?",
     FALSE, TRUE);
 
+#ifdef WHITELIST_D_MACROS
+/* Precompile the regular expression used to filter the content of macros
+given to -D for permissibility. */
+
+regex_whitelisted_macro =
+  regex_must_compile(US"^[A-Za-z0-9_/.-]*$", FALSE, TRUE);
+#endif
+
+
 /* If the program is called as "mailq" treat it as equivalent to "exim -bp";
 this seems to be a generally accepted convention, since one finds symbolic
 links called "mailq" in standard OS configurations. */
@@ -1486,8 +1710,20 @@ real_gid = getgid();
 
 if (real_uid == root_uid)
   {
-  setgid(real_gid);
-  setuid(real_uid);
+  rv = setgid(real_gid);
+  if (rv)
+    {
+    fprintf(stderr, "exim: setgid(%ld) failed: %s\n",
+        (long int)real_gid, strerror(errno));
+    exit(EXIT_FAILURE);
+    }
+  rv = setuid(real_uid);
+  if (rv)
+    {
+    fprintf(stderr, "exim: setuid(%ld) failed: %s\n",
+        (long int)real_uid, strerror(errno));
+    exit(EXIT_FAILURE);
+    }
   }
 
 /* If neither the original real uid nor the original euid was root, Exim is
@@ -1847,13 +2083,17 @@ for (i = 1; i < argc; i++)
       #endif
       if (real_uid != root_uid)
         {
-        #ifdef TRUSTED_CONFIG_PREFIX_LIST
+        #ifdef TRUSTED_CONFIG_LIST
 
-       if (Ustrstr(argrest, "/../"))
+        if (real_uid != exim_uid
+            #ifdef CONFIGURE_OWNER
+            && real_uid != config_uid
+            #endif
+            )
           trusted_config = FALSE;
         else
           {
-          FILE *trust_list = Ufopen(TRUSTED_CONFIG_PREFIX_LIST, "rb");
+          FILE *trust_list = Ufopen(TRUSTED_CONFIG_LIST, "rb");
           if (trust_list)
             {
             struct stat statbuf;
@@ -1879,8 +2119,8 @@ for (i = 1; i < argc; i++)
               {
               /* Well, the trust list at least is up to scratch... */
               void *reset_point = store_get(0);
-              uschar *trusted_prefixes[32];
-              int nr_prefixes = 0;
+              uschar *trusted_configs[32];
+              int nr_configs = 0;
               int i = 0;
 
               while (Ufgets(big_buffer, big_buffer_size, trust_list))
@@ -1893,13 +2133,13 @@ for (i = 1; i < argc; i++)
                 nl = Ustrchr(start, '\n');
                 if (nl)
                   *nl = 0;
-                trusted_prefixes[nr_prefixes++] = string_copy(start);
-                if (nr_prefixes == 32)
+                trusted_configs[nr_configs++] = string_copy(start);
+                if (nr_configs == 32)
                   break;
                 }
               fclose(trust_list);
 
-              if (nr_prefixes)
+              if (nr_configs)
                 {
                 int sep = 0;
                 uschar *list = argrest;
@@ -1907,14 +2147,12 @@ for (i = 1; i < argc; i++)
                 while (trusted_config && (filename = string_nextinlist(&list,
                         &sep, big_buffer, big_buffer_size)) != NULL)
                   {
-                  for (i=0; i < nr_prefixes; i++)
+                  for (i=0; i < nr_configs; i++)
                     {
-                    int len = Ustrlen(trusted_prefixes[i]);
-                    if (Ustrlen(filename) >= len &&
-                        Ustrncmp(filename, trusted_prefixes[i], len) == 0)
+                    if (Ustrcmp(filename, trusted_configs[i]) == 0)
                       break;
                     }
-                  if (i == nr_prefixes)
+                  if (i == nr_configs)
                     {
                     trusted_config = FALSE;
                     break;
@@ -3017,7 +3255,8 @@ if (debug_selector != 0)
     debug_printf("Exim version %s uid=%ld gid=%ld pid=%d D=%x\n",
       version_string, (long int)real_uid, (long int)real_gid, (int)getpid(),
       debug_selector);
-    show_whats_supported(stderr);
+    if (!version_printed)
+      show_whats_supported(stderr);
     }
   }
 
@@ -3097,6 +3336,11 @@ till after reading the config, which might specify the exim gid. Therefore,
 save the group list here first. */
 
 group_count = getgroups(NGROUPS_MAX, group_list);
+if (group_count < 0)
+  {
+  fprintf(stderr, "exim: getgroups() failed: %s\n", strerror(errno));
+  exit(EXIT_FAILURE);
+  }
 
 /* There is a fundamental difference in some BSD systems in the matter of
 groups. FreeBSD and BSDI are known to be different; NetBSD and OpenBSD are
@@ -3141,7 +3385,8 @@ values (such as the path name). If running in the test harness, pretend that
 configuration file changes and macro definitions haven't happened. */
 
 if ((                                            /* EITHER */
-    (!trusted_config || macros != NULL) &&       /* Config changed, and */
+    (!trusted_config ||                          /* Config changed, or */
+     !macros_trusted()) &&                       /*  impermissible macros and */
     real_uid != root_uid &&                      /* Not root, and */
     !running_in_test_harness                     /* Not fudged */
     ) ||                                         /*   OR   */
@@ -3158,9 +3403,13 @@ if ((                                            /* EITHER */
   and should be used for any logging information because attempts to write
   to the log will usually fail. To arrange this, we unset really_exim. However,
   if no stderr is available there is no point - we might as well have a go
-  at the log (if it fails, syslog will be written). */
+  at the log (if it fails, syslog will be written).
 
-  if (log_stderr != NULL) really_exim = FALSE;
+  Note that if the invoker is Exim, the logs remain available. Messing with
+  this causes unlogged successful deliveries.  */
+
+  if ((log_stderr != NULL) && (real_uid != exim_uid))
+    really_exim = FALSE;
   }
 
 /* Privilege is to be retained for the moment. It may be dropped later,
@@ -3358,7 +3607,7 @@ if (removed_privilege && (!trusted_config || macros != NULL) &&
   else
     log_write(0, LOG_MAIN|LOG_PANIC,
       "exim user lost privilege for using %s option",
-      (int)exim_uid, trusted_config? "-D" : "-C");
+      trusted_config? "-D" : "-C");
   }
 
 /* Start up Perl interpreter if Perl support is configured and there is a
@@ -3383,6 +3632,13 @@ if (opt_perl_at_start && opt_perl_startup != NULL)
   }
 #endif /* EXIM_PERL */
 
+/* Initialise lookup_list
+If debugging, already called above via version reporting.
+This does mean that debugging causes the list to be initialised while root.
+This *should* be harmless -- all modules are loaded from a fixed dir and
+it's code that would, if not a module, be part of Exim already. */
+init_lookup_list();
+
 /* Log the arguments of the call if the configuration file said so. This is
 a debugging feature for finding out what arguments certain MUAs actually use.
 Don't attempt it if logging is disabled, or if listing variables or if
@@ -3698,7 +3954,28 @@ if (!unprivileged &&                      /* originally had root AND */
 
 /* When we are retaining a privileged uid, we still change to the exim gid. */
 
-else setgid(exim_gid);
+else
+  {
+  int rv;
+  rv = setgid(exim_gid);
+  /* Impact of failure is that some stuff might end up with an incorrect group.
+  We track this for failures from root, since any attempt to change privilege
+  by root should succeed and failures should be examined.  For non-root,
+  there's no security risk.  For me, it's { exim -bV } on a just-built binary,
+  no need to complain then. */
+  if (rv == -1)
+    {
+    if (!(unprivileged || removed_privilege))
+      {
+      fprintf(stderr,
+          "exim: changing group failed: %s\n", strerror(errno));
+      exit(EXIT_FAILURE);
+      }
+    else
+      DEBUG(D_any) debug_printf("changing group to %ld failed: %s\n",
+          (long int)exim_gid, strerror(errno));
+    }
+  }
 
 /* Handle a request to scan a file for malware */
 if (malware_test_file)
@@ -4330,8 +4607,8 @@ if (expansion_test)
 
   else
     {
-    char *(*fn_readline)(char *) = NULL;
-    char *(*fn_addhist)(char *) = NULL;
+    char *(*fn_readline)(const char *) = NULL;
+    void (*fn_addhist)(const char *) = NULL;
 
     #ifdef USE_READLINE
     void *dlhandle = set_readline(&fn_readline, &fn_addhist);