Fix ClamAV command send
[exim.git] / src / src / malware.c
index 2883f225af4b7c526940c530ad65583f4a790ebc..e696f7a497d8c54473c464c12766d7f9ac3a3ca0 100644 (file)
@@ -109,7 +109,7 @@ features_malware(void)
 {
 const uschar * s;
 uschar * t;
-uschar buf[64];
+uschar buf[EXIM_DRIVERNAME_MAX];
 
 spf(buf, sizeof(buf), US"_HAVE_MALWARE_");
 
@@ -132,7 +132,6 @@ static const uschar * malware_regex_default = US ".+";
 static const pcre * malware_default_re = NULL;
 
 
-
 #ifndef DISABLE_MAL_CLAM
 /* The maximum number of clamd servers that are supported in the configuration */
 # define MAX_CLAMD_SERVERS 32
@@ -272,8 +271,19 @@ static inline int
 m_tcpsocket(const uschar * hostname, unsigned int port,
        host_item * host, uschar ** errstr, const blob * fastopen_blob)
 {
-return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5,
+int fd = ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5,
                          host, errstr, fastopen_blob);
+#ifdef EXIM_TFO_FREEBSD
+/* Under some fault conditions, FreeBSD 12.2 seen to send a (non-TFO) SYN
+and, getting no response, wait for a long time.  Impose a 5s max. */
+if (fd >= 0)
+  {
+  struct timeval tv = {.tv_sec = 5};
+  fd_set fds;
+  FD_ZERO(&fds); FD_SET(fd, &fds); (void) select(fd+1, NULL, &fds, NULL, &tv);
+  }
+#endif
+return fd;
 }
 #endif
 
@@ -1443,7 +1453,6 @@ badseek:  err = errno;
       uschar av_buffer[1024];
       uschar *hostname = US"";
       host_item connhost;
-      uschar *clamav_fbuf;
       int clam_fd, result;
       unsigned int fsize_uint;
       BOOL use_scan_command = FALSE;
@@ -1551,16 +1560,17 @@ badseek:  err = errno;
        { cmd_str.data = US"zINSTREAM"; cmd_str.len = 10; }
       else
        {
-       cmd_str.data = string_sprintf("SCAN %s\n", eml_filename);
-       cmd_str.len = Ustrlen(cmd_str.data);
+       int n;
+       cmd_str.data = string_sprintf("SCAN %s\n%n", eml_filename, &n);
+       cmd_str.len = n;                /* .len is a size_t */
        }
 
       /* We have some network servers specified */
       if (num_servers)
        {
        /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
-        * only supports AF_INET, but we should probably be looking to the
-        * future and rewriting this to be protocol-independent anyway. */
+       only supports AF_INET, but we should probably be looking to the
+       future and rewriting this to be protocol-independent anyway. */
 
        while (num_servers > 0)
          {
@@ -1571,16 +1581,17 @@ badseek:  err = errno;
                         cd->hostspec, cd->tcp_port);
 
          /* Lookup the host. This is to ensure that we connect to the same IP
-          * on both connections (as one host could resolve to multiple ips) */
+         on both connections (as one host could resolve to multiple ips) */
          for (;;)
            {
-           /*XXX we trust that the cmd_str is ideempotent */
+           /*XXX we trust that the cmd_str is idempotent */
            if ((malware_daemon_ctx.sock = m_tcpsocket(cd->hostspec, cd->tcp_port,
-                                   &connhost, &errstr, &cmd_str)) >= 0)
+                                   &connhost, &errstr,
+                                   use_scan_command ? &cmd_str : NULL)) >= 0)
              {
              /* Connection successfully established with a server */
              hostname = cd->hostspec;
-             cmd_str.len = 0;
+             if (use_scan_command) cmd_str.len = 0;
              break;
              }
            if (cd->retry <= 0) break;
@@ -1614,21 +1625,28 @@ badseek:  err = errno;
          }
 
       /* have socket in variable "sock"; command to use is semi-independent of
-       * the socket protocol.  We use SCAN if is local (either Unix/local
-       * domain socket, or explicitly told local) else we stream the data.
-       * How we stream the data depends upon how we were built.  */
+      the socket protocol.  We use SCAN if is local (either Unix/local
+      domain socket, or explicitly told local) else we stream the data.
+      How we stream the data depends upon how we were built.  */
 
       if (!use_scan_command)
        {
        struct stat st;
+#if defined(EXIM_TCP_CORK) && !defined(OS_SENDFILE)
+       BOOL corked = TRUE;
+#endif
        /* New protocol: "zINSTREAM\n" followed by a sequence of <length><data>
        chunks, <n> a 4-byte number (network order), terminated by a zero-length
-       chunk. */
+       chunk. We only send one chunk. */
 
        DEBUG(D_acl) debug_printf_indent(
            "Malware scan: issuing %s new-style remote scan (zINSTREAM)\n",
            scanner_name);
 
+#if defined(EXIM_TCP_CORK)
+       (void) setsockopt(malware_daemon_ctx.sock, IPPROTO_TCP, EXIM_TCP_CORK,
+                         US &on, sizeof(on));
+#endif
        /* Pass the string to ClamAV (10 = "zINSTREAM\0"), if not already sent */
        if (cmd_str.len)
          if (send(malware_daemon_ctx.sock, cmd_str.data, cmd_str.len, 0) < 0)
@@ -1663,26 +1681,45 @@ badseek:  err = errno;
            malware_daemon_ctx.sock);
          }
 
-       /* send file body to socket */
+       /* send file size */
        send_size = htonl(fsize_uint);
        if (send(malware_daemon_ctx.sock, &send_size, sizeof(send_size), 0) < 0)
          return m_panic_defer_3(scanent, NULL,
            string_sprintf("unable to send file size to socket (%s)", hostname),
            malware_daemon_ctx.sock);
 
+       /* send file body */
        while (fsize_uint)
          {
-         unsigned n = MIN(fsize_uint, big_buffer_size);
+#ifdef OS_SENDFILE
+         int n = os_sendfile(malware_daemon_ctx.sock, clam_fd, NULL, (size_t)fsize_uint);
+         if (n < 0)
+           return m_panic_defer_3(scanent, NULL,
+             string_sprintf("unable to send file body to socket (%s): %s", hostname, strerror(errno)),
+             malware_daemon_ctx.sock);
+         fsize_uint -= n;
+#else
+         int n = MIN(fsize_uint, big_buffer_size);
          if ((n = read(clam_fd, big_buffer, n)) < 0)
            return m_panic_defer_3(scanent, NULL,
              string_sprintf("can't read spool file %s: %s",
                eml_filename, strerror(errno)),
              malware_daemon_ctx.sock);
-         if ((n = send(malware_daemon_ctx.sock, clamav_fbuf, n, 0)) < 0)
+         if (send(malware_daemon_ctx.sock, big_buffer, (size_t)n, 0) < 0)
            return m_panic_defer_3(scanent, NULL,
-             string_sprintf("unable to send file body to socket (%s)", hostname),
+             string_sprintf("unable to send file body to socket (%s): %s", hostname, strerror(errno)),
              malware_daemon_ctx.sock);
          fsize_uint -= n;
+# ifdef EXIM_TCP_CORK
+         if (corked)
+           {
+           corked = FALSE;
+           (void) setsockopt(malware_daemon_ctx.sock, IPPROTO_TCP, EXIM_TCP_CORK,
+                             US &off, sizeof(off));
+           }
+# endif
+#endif /*!OS_SENDFILE*/
+
          }
 
        send_final_zeroblock = 0;
@@ -1690,11 +1727,15 @@ badseek:  err = errno;
          return m_panic_defer_3(scanent, NULL,
            string_sprintf("unable to send file terminator to socket (%s)", hostname),
            malware_daemon_ctx.sock);
+#ifdef OS_SENDFILE
+       (void) setsockopt(malware_daemon_ctx.sock, IPPROTO_TCP, EXIM_TCP_CORK,
+                         US &off, sizeof(off));
+#endif
        }
       else
        { /* use scan command */
        /* Send a SCAN command pointing to a filename; then in the then in the
-        * scan-method-neutral part, read the response back */
+       scan-method-neutral part, read the response back */
 
 /* ================================================================= */
 
@@ -1719,10 +1760,10 @@ badseek:  err = errno;
              malware_daemon_ctx.sock);
 
        /* Do not shut down the socket for writing; a user report noted that
-        * clamd 0.70 does not react well to this. */
+       clamd 0.70 does not react well to this. */
        }
       /* Commands have been sent, no matter which scan method or connection
-       * type we're using; now just read the result, independent of method. */
+      type we're using; now just read the result, independent of method. */
 
       /* Read the result */
       memset(av_buffer, 0, sizeof(av_buffer));
@@ -1768,6 +1809,7 @@ badseek:  err = errno;
       /* strip newline at the end (won't be present for zINSTREAM)
       (also any trailing whitespace, which shouldn't exist, but we depend upon
       this below, so double-check) */
+
       p = av_buffer + Ustrlen(av_buffer) - 1;
       if (*p == '\n') *p = '\0';
 
@@ -1778,7 +1820,7 @@ badseek:  err = errno;
       if (*p) ++p;
 
       /* colon in returned output? */
-      if(!(p = Ustrchr(av_buffer,':')))
+      if (!(p = Ustrchr(av_buffer,':')))
        return m_panic_defer(scanent, CUS callout_address, string_sprintf(
                  "ClamAV returned malformed result (missing colon): %s",
                  av_buffer));