X-Git-Url: https://vcs.fsf.org/?p=exim.git;a=blobdiff_plain;f=src%2Fsrc%2Fmalware.c;h=7ed4d358d983db82c30c7efd0d9f1c89c67f8f27;hp=30586feda08f32ba134f6ac15991e0d6bc334dfd;hb=a8e46b3b7147d214072bc34beff184724f3f2be6;hpb=9439f85e4de0ea4cd0ddedad9387e50c935073e2 diff --git a/src/src/malware.c b/src/src/malware.c index 30586feda..7ed4d358d 100644 --- a/src/src/malware.c +++ b/src/src/malware.c @@ -2,16 +2,55 @@ * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) Tom Kistner 2003-2014 */ -/* License: GPL */ +/* Copyright (c) Tom Kistner 2003 - 2015 + * License: GPL + * Copyright (c) The Exim Maintainers 2015 - 2020 + */ /* Code for calling virus (malware) scanners. Called from acl.c. */ #include "exim.h" -#ifdef WITH_CONTENT_SCAN +#ifdef WITH_CONTENT_SCAN /* entire file */ -typedef enum {M_FPROTD, M_DRWEB, M_AVES, M_FSEC, M_KAVD, M_CMDL, - M_SOPHIE, M_CLAMD, M_SOCK, M_MKSD, M_AVAST} scanner_t; +typedef enum { +#ifndef DISABLE_MAL_FFROTD + M_FPROTD, +#endif +#ifndef DISABLE_MAL_FFROT6D + M_FPROT6D, +#endif +#ifndef DISABLE_MAL_DRWEB + M_DRWEB, +#endif +#ifndef DISABLE_MAL_AVE + M_AVES, +#endif +#ifndef DISABLE_MAL_FSECURE + M_FSEC, +#endif +#ifndef DISABLE_MAL_KAV + M_KAVD, +#endif +#ifndef DISABLE_MAL_SOPHIE + M_SOPHIE, +#endif +#ifndef DISABLE_MAL_CLAM + M_CLAMD, +#endif +#ifndef DISABLE_MAL_MKS + M_MKSD, +#endif +#ifndef DISABLE_MAL_AVAST + M_AVAST, +#endif +#ifndef DISABLE_MAL_SOCK + M_SOCK, +#endif +#ifndef DISABLE_MAL_CMDLINE + M_CMDL, +#endif + M_DUMMY + } scanner_t; typedef enum {MC_NONE, MC_TCP, MC_UNIX, MC_STRM} contype_t; static struct scan { @@ -21,66 +60,153 @@ static struct scan contype_t conn; } m_scans[] = { +#ifndef DISABLE_MAL_FFROTD { M_FPROTD, US"f-protd", US"localhost 10200-10204", MC_TCP }, +#endif +#ifndef DISABLE_MAL_FFROT6D + { M_FPROT6D, US"f-prot6d", US"localhost 10200", MC_TCP }, +#endif +#ifndef DISABLE_MAL_DRWEB { M_DRWEB, US"drweb", US"/usr/local/drweb/run/drwebd.sock", MC_STRM }, +#endif +#ifndef DISABLE_MAL_AVE { M_AVES, US"aveserver", US"/var/run/aveserver", MC_UNIX }, +#endif +#ifndef DISABLE_MAL_FSECURE { M_FSEC, US"fsecure", US"/var/run/.fsav", MC_UNIX }, +#endif +#ifndef DISABLE_MAL_KAV { M_KAVD, US"kavdaemon", US"/var/run/AvpCtl", MC_UNIX }, - { M_CMDL, US"cmdline", NULL, MC_NONE }, +#endif +#ifndef DISABLE_MAL_SOPHIE { M_SOPHIE, US"sophie", US"/var/run/sophie", MC_UNIX }, +#endif +#ifndef DISABLE_MAL_CLAM { M_CLAMD, US"clamd", US"/tmp/clamd", MC_NONE }, - { M_SOCK, US"sock", US"/tmp/malware.sock", MC_STRM }, +#endif +#ifndef DISABLE_MAL_MKS { M_MKSD, US"mksd", NULL, MC_NONE }, +#endif +#ifndef DISABLE_MAL_AVAST { M_AVAST, US"avast", US"/var/run/avast/scan.sock", MC_STRM }, +#endif +#ifndef DISABLE_MAL_SOCK + { M_SOCK, US"sock", US"/tmp/malware.sock", MC_STRM }, +#endif +#ifndef DISABLE_MAL_CMDLINE + { M_CMDL, US"cmdline", NULL, MC_NONE }, +#endif { -1, NULL, NULL, MC_NONE } /* end-marker */ }; +/******************************************************************************/ +# ifdef MACRO_PREDEF /* build solely to predefine macros */ + +# include "macro_predef.h" + +void +features_malware(void) +{ +const uschar * s; +uschar * t; +uschar buf[64]; + +spf(buf, sizeof(buf), US"_HAVE_MALWARE_"); + +for (const struct scan * sc = m_scans; sc->scancode != -1; sc++) + { + for (s = sc->name, t = buf+14; *s; s++) if (*s != '-') + *t++ = toupper(*s); + *t = '\0'; + builtin_macro_create(buf); + } +} + +/******************************************************************************/ +# else /*!MACRO_PREDEF, main build*/ + + +#define MALWARE_TIMEOUT 120 /* default timeout, seconds */ + +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 -#define MAX_CLAMD_SERVERS_S "32" -/* Maximum length of the hostname that can be specified in the clamd address list */ -#define MAX_CLAMD_ADDRESS_LENGTH 64 -#define MAX_CLAMD_ADDRESS_LENGTH_S "64" - -typedef struct clamd_address_container { - uschar tcp_addr[MAX_CLAMD_ADDRESS_LENGTH+1]; - unsigned int tcp_port; -} clamd_address_container; - -/* declaration of private routines */ -static int mksd_scan_packed(struct scan * scanent, int sock, uschar *scan_filename, int tmo); -static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking); - -#ifndef nelements -# define nelements(arr) (sizeof(arr) / sizeof(arr[0])) +# define MAX_CLAMD_SERVERS 32 +# define MAX_CLAMD_SERVERS_S "32" + +typedef struct clamd_address { + uschar * hostspec; + unsigned tcp_port; + unsigned retry; +} clamd_address; #endif -#define MALWARE_TIMEOUT 120 +#ifndef DISABLE_MAL_DRWEB +# define DRWEBD_SCAN_CMD (1) /* scan file, buffer or diskfile */ +# define DRWEBD_RETURN_VIRUSES (1<<0) /* ask daemon return to us viruses names from report */ +# define DRWEBD_IS_MAIL (1<<19) /* say to daemon that format is "archive MAIL" */ + +# define DERR_READ_ERR (1<<0) /* read error */ +# define DERR_NOMEMORY (1<<2) /* no memory */ +# define DERR_TIMEOUT (1<<9) /* scan timeout has run out */ +# define DERR_BAD_CALL (1<<15) /* wrong command */ +static const uschar * drweb_re_str = US "infected\\swith\\s*(.+?)$"; +static const pcre * drweb_re = NULL; +#endif -#define DRWEBD_SCAN_CMD (1) /* scan file, buffer or diskfile */ -#define DRWEBD_RETURN_VIRUSES (1<<0) /* ask daemon return to us viruses names from report */ -#define DRWEBD_IS_MAIL (1<<19) /* say to daemon that format is "archive MAIL" */ +#ifndef DISABLE_MAL_FSECURE +static const uschar * fsec_re_str = US "\\S{0,5}INFECTED\\t[^\\t]*\\t([^\\t]+)\\t\\S*$"; +static const pcre * fsec_re = NULL; +#endif -#define DERR_READ_ERR (1<<0) /* read error */ -#define DERR_NOMEMORY (1<<2) /* no memory */ -#define DERR_TIMEOUT (1<<9) /* scan timeout has run out */ -#define DERR_BAD_CALL (1<<15) /* wrong command */ +#ifndef DISABLE_MAL_KAV +static const uschar * kav_re_sus_str = US "suspicion:\\s*(.+?)\\s*$"; +static const uschar * kav_re_inf_str = US "infected:\\s*(.+?)\\s*$"; +static const pcre * kav_re_sus = NULL; +static const pcre * kav_re_inf = NULL; +#endif +#ifndef DISABLE_MAL_AVAST +static const uschar * ava_re_clean_str = US "(?!\\\\)\\t\\[\\+\\]"; +static const uschar * ava_re_virus_str = US "(?!\\\\)\\t\\[L\\]\\d+\\.0\\t0\\s(.*)"; +static const uschar * ava_re_error_str = US "(?!\\\\)\\t\\[E\\]\\d+\\.0\\tError\\s\\d+\\s(.*)"; +static const pcre * ava_re_clean = NULL; +static const pcre * ava_re_virus = NULL; +static const pcre * ava_re_error = NULL; +#endif + +#ifndef DISABLE_MAL_FFROT6D +static const uschar * fprot6d_re_error_str = US "^\\d+\\s<(.+?)>$"; +static const uschar * fprot6d_re_virus_str = US "^\\d+\\s\\s+.+$"; +static const pcre * fprot6d_re_error = NULL; +static const pcre * fprot6d_re_virus = NULL; +#endif + + + +/******************************************************************************/ + +#ifndef DISABLE_MAL_KAV /* Routine to check whether a system is big- or little-endian. Ripped from http://www.faqs.org/faqs/graphics/fileformats-faq/part4/section-7.html Needed for proper kavdaemon implementation. Sigh. */ -#define BIG_MY_ENDIAN 0 -#define LITTLE_MY_ENDIAN 1 +# define BIG_MY_ENDIAN 0 +# define LITTLE_MY_ENDIAN 1 static int test_byte_order(void); static inline int test_byte_order() { short int word = 0x0001; - char *byte = (char *) &word; + char *byte = CS &word; return(byte[0] ? LITTLE_MY_ENDIAN : BIG_MY_ENDIAN); } +#endif BOOL malware_ok = FALSE; @@ -88,263 +214,341 @@ BOOL malware_ok = FALSE; the scan directory normally for that case, but look into rigging up the needed header variables if not already set on the command-line? */ extern int spool_mbox_ok; -extern uschar spooled_message_id[17]; - -/************************************************* -* Scan an email for malware * -*************************************************/ +extern uschar spooled_message_id[MESSAGE_ID_LENGTH+1]; -/* This is the normal interface for scanning an email, which doesn't need a -filename; it's a wrapper around the malware_file function. -Arguments: - listptr the list of options to the "malware = ..." ACL condition +/* Some (currently avast only) use backslash escaped whitespace, +this function undoes these escapes */ -Returns: Exim message processing code (OK, FAIL, DEFER, ...) - where true means malware was found (condition applies) -*/ -int -malware(uschar **listptr) +static inline void +unescape(uschar *p) { - uschar * scan_filename; - int ret; - - scan_filename = string_sprintf("%s/scan/%s/%s.eml", - spool_directory, message_id, message_id); - ret = malware_internal(listptr, scan_filename, FALSE); - if (ret == DEFER) av_failed = TRUE; - - return ret; +uschar *p0; +for (; *p; ++p) + if (*p == '\\' && (isspace(p[1]) || p[1] == '\\')) + for (p0 = p; *p0; ++p0) *p0 = p0[1]; } - -/************************************************* -* Scan a file for malware * -*************************************************/ - -/* This is a test wrapper for scanning an email, which is not used in -normal processing. Scan any file, using the Exim scanning interface. -This function tampers with various global variables so is unsafe to use -in any other context. - -Arguments: - eml_filename a file holding the message to be scanned - -Returns: Exim message processing code (OK, FAIL, DEFER, ...) - where true means malware was found (condition applies) -*/ -int -malware_in_file(uschar *eml_filename) +/* --- malware_*_defer --- */ +static inline int +malware_panic_defer(const uschar * str) { - uschar *scan_options[2]; - uschar message_id_buf[64]; - int ret; - - scan_options[0] = US"*"; - scan_options[1] = NULL; - - /* spool_mbox() assumes various parameters exist, when creating - the relevant directory and the email within */ - (void) string_format(message_id_buf, sizeof(message_id_buf), - "dummy-%d", vaguely_random_number(INT_MAX)); - message_id = message_id_buf; - sender_address = US"malware-sender@example.net"; - return_path = US""; - recipients_list = NULL; - receive_add_recipient(US"malware-victim@example.net", -1); - enable_dollar_recipients = TRUE; - - ret = malware_internal(scan_options, eml_filename, TRUE); - - Ustrncpy(spooled_message_id, message_id, sizeof(spooled_message_id)); - spool_mbox_ok = 1; - /* don't set no_mbox_unspool; at present, there's no way for it to become - set, but if that changes, then it should apply to these tests too */ - unspool_mbox(); - - /* silence static analysis tools */ - message_id = NULL; - - return ret; +log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: %s", str); +return DEFER; } - - static inline int -malware_errlog_defer(const uschar * str) +malware_log_defer(const uschar * str) { - log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: %s", str); - return DEFER; +log_write(0, LOG_MAIN, "malware acl condition: %s", str); +return DEFER; } - -static int -m_errlog_defer(struct scan * scanent, const uschar * str) +/* --- m_*_defer --- */ +static inline int +m_panic_defer(struct scan * scanent, const uschar * hostport, + const uschar * str) { - return malware_errlog_defer(string_sprintf("%s: %s", scanent->name, str)); +return malware_panic_defer(string_sprintf("%s %s : %s", + scanent->name, hostport ? hostport : CUS"", str)); } -static int -m_errlog_defer_3(struct scan * scanent, const uschar * str, - int fd_to_close) +static inline int +m_log_defer(struct scan * scanent, const uschar * hostport, + const uschar * str) +{ +return malware_log_defer(string_sprintf("%s %s : %s", + scanent->name, hostport ? hostport : CUS"", str)); +} +/* --- m_*_defer_3 */ +static inline int +m_panic_defer_3(struct scan * scanent, const uschar * hostport, + const uschar * str, int fd_to_close) { - (void) close(fd_to_close); - return m_errlog_defer(scanent, str); +DEBUG(D_acl) debug_print_socket(fd_to_close); +(void) close(fd_to_close); +return m_panic_defer(scanent, hostport, str); } /*************************************************/ +#ifndef DISABLE_MAL_CLAM /* Only used by the Clamav code, which is working from a list of servers and uses the returned in_addr to get a second connection to the same system. */ static inline int m_tcpsocket(const uschar * hostname, unsigned int port, - host_item * host, uschar ** errstr) -{ - return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5, host, errstr); -} - -static int -m_tcpsocket_fromdef(const uschar * hostport, uschar ** errstr) -{ - int scan; - uschar hostname[256]; - unsigned int portlow, porthigh; - - /* extract host and port part */ - scan = sscanf(CS hostport, "%255s %u-%u", hostname, &portlow, &porthigh); - if ( scan != 3 ) { - if ( scan != 2 ) { - *errstr = string_sprintf("invalid socket '%s'", hostport); - return -1; - } - porthigh = portlow; - } - - return ip_connectedsocket(SOCK_STREAM, hostname, portlow, porthigh, - 5, NULL, errstr); -} - -static int -m_unixsocket(const uschar * path, uschar ** errstr) + host_item * host, uschar ** errstr, const blob * fastopen_blob) { - int sock; - struct sockaddr_un server; - - if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { - *errstr = US"can't open UNIX socket."; - return -1; - } - - server.sun_family = AF_UNIX; - Ustrncpy(server.sun_path, path, sizeof(server.sun_path)-1); - server.sun_path[sizeof(server.sun_path)-1] = '\0'; - if (connect(sock, (struct sockaddr *) &server, sizeof(server)) < 0) { - int err = errno; - (void)close(sock); - *errstr = string_sprintf("unable to connect to UNIX socket (%s): %s", - path, strerror(err)); - return -1; - } - return sock; -} - -static inline int -m_streamsocket(const uschar * spec, uschar ** errstr) -{ - return *spec == '/' - ? m_unixsocket(spec, errstr) : m_tcpsocket_fromdef(spec, errstr); +return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5, + host, errstr, fastopen_blob); } +#endif static int m_sock_send(int sock, uschar * buf, int cnt, uschar ** errstr) { - if (send(sock, buf, cnt, 0) < 0) { - int err = errno; - (void)close(sock); - *errstr = string_sprintf("unable to send to socket (%s): %s", - buf, strerror(err)); - return -1; - } - return sock; +if (send(sock, buf, cnt, 0) < 0) + { + int err = errno; + (void)close(sock); + *errstr = string_sprintf("unable to send to socket (%s): %s", + buf, strerror(err)); + return -1; + } +return sock; } static const pcre * m_pcre_compile(const uschar * re, uschar ** errstr) { - const uschar * rerror; - int roffset; - const pcre * cre; - - cre = pcre_compile(CS re, PCRE_COPT, (const char **)&rerror, &roffset, NULL); - if (!cre) - *errstr= string_sprintf("regular expression error in '%s': %s at offset %d", - re, rerror, roffset); - return cre; +const uschar * rerror; +int roffset; +const pcre * cre; + +if (!(cre = pcre_compile(CS re, PCRE_COPT, CCSS &rerror, &roffset, NULL))) + *errstr= string_sprintf("regular expression error in '%s': %s at offset %d", + re, rerror, roffset); +return cre; } uschar * m_pcre_exec(const pcre * cre, uschar * text) { - int ovector[10*3]; - int i = pcre_exec(cre, NULL, CS text, Ustrlen(text), 0, 0, - ovector, nelements(ovector)); - uschar * substr = NULL; - if (i >= 2) /* Got it */ - pcre_get_substring(CS text, ovector, i, 1, (const char **) &substr); - return substr; +int ovector[10*3]; +int i = pcre_exec(cre, NULL, CS text, Ustrlen(text), 0, 0, + ovector, nelem(ovector)); +uschar * substr = NULL; +if (i >= 2) /* Got it */ + pcre_get_substring(CS text, ovector, i, 1, CCSS &substr); +return substr; } static const pcre * -m_pcre_nextinlist(uschar ** list, int * sep, char * listerr, uschar ** errstr) +m_pcre_nextinlist(const uschar ** list, int * sep, + char * listerr, uschar ** errstr) { - const uschar * list_ele; - const pcre * cre = NULL; +const uschar * list_ele; +const pcre * cre = NULL; - if (!(list_ele = string_nextinlist(list, sep, NULL, 0))) - *errstr = US listerr; - else - cre = m_pcre_compile(CUS list_ele, errstr); - return cre; +if (!(list_ele = string_nextinlist(list, sep, NULL, 0))) + *errstr = US listerr; +else + { + DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "RE: ", + string_printing(list_ele)); + cre = m_pcre_compile(CUS list_ele, errstr); + } +return cre; } + /* Simple though inefficient wrapper for reading a line. Drop CRs and the trailing newline. Can return early on buffer full. Null-terminate. Apply initial timeout if no data ready. - Return: number of chars - zero for an empty line or EOF + Return: number of chars - zero for an empty line + -1 on EOF + -2 on timeout or error */ static int -recv_line(int fd, uschar * buffer, int bsize, int tmo) +recv_line(int fd, uschar * buffer, int bsize, time_t tmo) { uschar * p = buffer; ssize_t rcv; +BOOL ok = FALSE; -if (!fd_ready(fd, tmo-time(NULL))) - return -1; +if (!fd_ready(fd, tmo)) + return -2; - /*XXX tmo handling assumes we always get a whole line */ +/*XXX tmo handling assumes we always get a whole line */ /* read until \n */ +errno = 0; while ((rcv = read(fd, p, 1)) > 0) { + ok = TRUE; if (p-buffer > bsize-2) break; if (*p == '\n') break; if (*p != '\r') p++; } +if (!ok) + { + DEBUG(D_acl) + { + debug_printf_indent("Malware scan: read %s (%s)\n", + rcv==0 ? "EOF" : "error", strerror(errno)); + debug_print_socket(fd); + } + return rcv==0 ? -1 : -2; + } *p = '\0'; -DEBUG(D_acl) debug_printf("Malware scan: read '%s'\n", buffer); +DEBUG(D_acl) debug_printf_indent("Malware scan: read '%s'\n", buffer); return p - buffer; } /* return TRUE iff size as requested */ static BOOL -recv_len(int sock, void * buf, int size, int tmo) +recv_len(int sock, void * buf, int size, time_t tmo) { -return fd_ready(sock, tmo-time(NULL)) +return fd_ready(sock, tmo) ? recv(sock, buf, size, 0) == size : FALSE; } + +#ifndef DISABLE_MAL_MKS +/* ============= private routines for the "mksd" scanner type ============== */ + +# include + +static inline int +mksd_writev (int sock, struct iovec * iov, int iovcnt) +{ +int i; + +for (;;) + { + do + i = writev (sock, iov, iovcnt); + while (i < 0 && errno == EINTR); + if (i <= 0) + { + (void) malware_panic_defer( + US"unable to write to mksd UNIX socket (/var/run/mksd/socket)"); + return -1; + } + for (;;) /* check for short write */ + if (i >= iov->iov_len) + { + if (--iovcnt == 0) + return 0; + i -= iov->iov_len; + iov++; + } + else + { + iov->iov_len -= i; + iov->iov_base = CS iov->iov_base + i; + break; + } + } +} + +static inline int +mksd_read_lines (int sock, uschar *av_buffer, int av_buffer_size, time_t tmo) +{ +client_conn_ctx cctx = {.sock = sock}; +int offset = 0; +int i; + +do + { + i = ip_recv(&cctx, av_buffer+offset, av_buffer_size-offset, tmo); + if (i <= 0) + { + (void) malware_panic_defer(US"unable to read from mksd UNIX socket (/var/run/mksd/socket)"); + return -1; + } + + offset += i; + /* offset == av_buffer_size -> buffer full */ + if (offset == av_buffer_size) + { + (void) malware_panic_defer(US"malformed reply received from mksd"); + return -1; + } + } while (av_buffer[offset-1] != '\n'); + +av_buffer[offset] = '\0'; +return offset; +} + +static inline int +mksd_parse_line(struct scan * scanent, char * line) +{ +char *p; + +switch (*line) + { + case 'O': /* OK */ + return OK; + + case 'E': + case 'A': /* ERR */ + if ((p = strchr (line, '\n')) != NULL) + *p = '\0'; + return m_panic_defer(scanent, NULL, + string_sprintf("scanner failed: %s", line)); + + default: /* VIR */ + if ((p = strchr (line, '\n')) != NULL) + { + *p = '\0'; + if ( p-line > 5 + && line[3] == ' ' + && (p = strchr(line+4, ' ')) != NULL + && p-line > 4 + ) + { + *p = '\0'; + malware_name = string_copy(US line+4); + return OK; + } + } + return m_panic_defer(scanent, NULL, + string_sprintf("malformed reply received: %s", line)); + } +} + +static int +mksd_scan_packed(struct scan * scanent, int sock, const uschar * scan_filename, + time_t tmo) +{ +struct iovec iov[3]; +const char *cmd = "MSQ\n"; +uschar av_buffer[1024]; + +iov[0].iov_base = (void *) cmd; +iov[0].iov_len = 3; +iov[1].iov_base = (void *) scan_filename; +iov[1].iov_len = Ustrlen(scan_filename); +iov[2].iov_base = (void *) (cmd + 3); +iov[2].iov_len = 1; + +if (mksd_writev (sock, iov, 3) < 0) + return DEFER; + +if (mksd_read_lines (sock, av_buffer, sizeof (av_buffer), tmo) < 0) + return DEFER; + +return mksd_parse_line (scanent, CS av_buffer); +} +#endif /* MKSD */ + + +#ifndef DISABLE_MAL_CLAM +static int +clamd_option(clamd_address * cd, const uschar * optstr, int * subsep) +{ +uschar * s; + +cd->retry = 0; +while ((s = string_nextinlist(&optstr, subsep, NULL, 0))) + if (Ustrncmp(s, "retry=", 6) == 0) + { + int sec = readconf_readtime((s += 6), '\0', FALSE); + if (sec < 0) + return FAIL; + cd->retry = sec; + } + else + return FAIL; +return OK; +} +#endif + + + /************************************************* * Scan content for malware * *************************************************/ @@ -353,1311 +557,1421 @@ return fd_ready(sock, tmo-time(NULL)) is via malware(), or there's malware_in_file() used for testing/debugging. Arguments: - listptr the list of options to the "malware = ..." ACL condition - eml_filename the file holding the email to be scanned - faking whether or not we're faking this up for the -bmalware test + malware_re match condition for "malware=" + scan_filename the file holding the email to be scanned, if we're faking + this up for the -bmalware test, else NULL + timeout if nonzero, non-default timeoutl Returns: Exim message processing code (OK, FAIL, DEFER, ...) where true means malware was found (condition applies) */ static int -malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking) +malware_internal(const uschar * malware_re, const uschar * scan_filename, + int timeout) { - int sep = 0; - uschar *list = *listptr; - uschar *av_scanner_work = av_scanner; - uschar *scanner_name; - uschar *malware_regex; - uschar malware_regex_default[] = ".+"; - unsigned long mbox_size; - FILE *mbox_file; - const pcre *re; - uschar * errstr; - struct scan * scanent; - const uschar * scanner_options; - int sock = -1; - time_t tmo; - - /* make sure the eml mbox file is spooled up */ - if (!(mbox_file = spool_mbox(&mbox_size, faking ? eml_filename : NULL))) - return malware_errlog_defer(US"error while creating mbox spool file"); - - /* none of our current scanners need the mbox - file as a stream, so we can close it right away */ - (void)fclose(mbox_file); - - /* extract the malware regex to match against from the option list */ - if (!(malware_regex = string_nextinlist(&list, &sep, NULL, 0))) - return FAIL; /* empty means "don't match anything" */ - - /* parse 1st option */ - if ( (strcmpic(malware_regex,US"false") == 0) || - (Ustrcmp(malware_regex,"0") == 0) ) - return FAIL; /* explicitly no matching */ - - /* special cases (match anything except empty) */ - if ( (strcmpic(malware_regex,US"true") == 0) || - (Ustrcmp(malware_regex,"*") == 0) || - (Ustrcmp(malware_regex,"1") == 0) ) - malware_regex = malware_regex_default; - - /* Reset sep that is set by previous string_nextinlist() call */ - sep = 0; - - /* compile the regex, see if it works */ - if (!(re = m_pcre_compile(malware_regex, &errstr))) - return malware_errlog_defer(errstr); - - /* if av_scanner starts with a dollar, expand it first */ - if (*av_scanner == '$') { - if (!(av_scanner_work = expand_string(av_scanner))) - return malware_errlog_defer( - string_sprintf("av_scanner starts with $, but expansion failed: %s", - expand_string_message)); - - debug_printf("Expanded av_scanner global: %s\n", av_scanner_work); - /* disable result caching in this case */ - malware_name = NULL; - malware_ok = FALSE; +int sep = 0; +const uschar *av_scanner_work = av_scanner; +uschar *scanner_name; +unsigned long mbox_size; +FILE *mbox_file; +const pcre *re; +uschar * errstr; +struct scan * scanent; +const uschar * scanner_options; +client_conn_ctx malware_daemon_ctx = {.sock = -1}; +time_t tmo; +uschar * eml_filename, * eml_dir; + +if (!malware_re) + return FAIL; /* empty means "don't match anything" */ + +/* Ensure the eml mbox file is spooled up */ + +if (!(mbox_file = spool_mbox(&mbox_size, scan_filename, &eml_filename))) + return malware_panic_defer(US"error while creating mbox spool file"); + +/* None of our current scanners need the mbox file as a stream (they use +the name), so we can close it right away. Get the directory too. */ + +(void) fclose(mbox_file); +eml_dir = string_copyn(eml_filename, Ustrrchr(eml_filename, '/') - eml_filename); + +/* parse 1st option */ +if (strcmpic(malware_re, US"false") == 0 || Ustrcmp(malware_re,"0") == 0) + return FAIL; /* explicitly no matching */ + +/* special cases (match anything except empty) */ +if ( strcmpic(malware_re,US"true") == 0 + || Ustrcmp(malware_re,"*") == 0 + || Ustrcmp(malware_re,"1") == 0 + ) + { + if ( !malware_default_re + && !(malware_default_re = m_pcre_compile(malware_regex_default, &errstr))) + return malware_panic_defer(errstr); + malware_re = malware_regex_default; + re = malware_default_re; } - /* Do not scan twice (unless av_scanner is dynamic). */ - if (!malware_ok) { - - /* find the scanner type from the av_scanner option */ - if (!(scanner_name = string_nextinlist(&av_scanner_work, &sep, NULL, 0))) - return malware_errlog_defer(US"av_scanner configuration variable is empty"); - tmo = time(NULL) + MALWARE_TIMEOUT; - - for (scanent = m_scans; ; scanent++) { - if (!scanent->name) - return malware_errlog_defer(string_sprintf("unknown scanner type '%s'", - scanner_name)); - if (strcmpic(scanner_name, US scanent->name) != 0) - continue; - if (!(scanner_options = string_nextinlist(&av_scanner_work, &sep, NULL, 0))) - scanner_options = scanent->options_default; - if (scanent->conn == MC_NONE) - break; - switch(scanent->conn) - { - case MC_TCP: sock = m_tcpsocket_fromdef(scanner_options, &errstr); break; - case MC_UNIX: sock = m_unixsocket(scanner_options, &errstr); break; - case MC_STRM: sock = m_streamsocket(scanner_options, &errstr); break; - default: /* compiler quietening */ break; - } - if (sock < 0) - return m_errlog_defer(scanent, errstr); +/* compile the regex, see if it works */ +else if (!(re = m_pcre_compile(malware_re, &errstr))) + return malware_panic_defer(errstr); + +/* if av_scanner starts with a dollar, expand it first */ +if (*av_scanner == '$') + { + if (!(av_scanner_work = expand_string(av_scanner))) + return malware_panic_defer( + string_sprintf("av_scanner starts with $, but expansion failed: %s", + expand_string_message)); + + DEBUG(D_acl) + debug_printf_indent("Expanded av_scanner global: %s\n", av_scanner_work); + /* disable result caching in this case */ + malware_name = NULL; + malware_ok = FALSE; + } + +/* Do not scan twice (unless av_scanner is dynamic). */ +if (!malware_ok) + { + /* find the scanner type from the av_scanner option */ + if (!(scanner_name = string_nextinlist(&av_scanner_work, &sep, NULL, 0))) + return malware_panic_defer(US"av_scanner configuration variable is empty"); + if (!timeout) timeout = MALWARE_TIMEOUT; + tmo = time(NULL) + timeout; + + for (scanent = m_scans; ; scanent++) + { + if (!scanent->name) + return malware_panic_defer(string_sprintf("unknown scanner type '%s'", + scanner_name)); + if (strcmpic(scanner_name, US scanent->name) != 0) + continue; + DEBUG(D_acl) debug_printf_indent("Malware scan: %s tmo=%s\n", + scanner_name, readconf_printtime(timeout)); + + if (!(scanner_options = string_nextinlist(&av_scanner_work, &sep, NULL, 0))) + scanner_options = scanent->options_default; + if (scanent->conn == MC_NONE) break; + + DEBUG(D_acl) debug_printf_indent("%15s%10s%s\n", "", "socket: ", scanner_options); + switch(scanent->conn) + { + case MC_TCP: + malware_daemon_ctx.sock = ip_tcpsocket(scanner_options, &errstr, 5, NULL); break; + case MC_UNIX: + malware_daemon_ctx.sock = ip_unixsocket(scanner_options, &errstr); break; + case MC_STRM: + malware_daemon_ctx.sock = ip_streamsocket(scanner_options, &errstr, 5, NULL); break; + default: + /* compiler quietening */ break; } - DEBUG(D_lookup) debug_printf("Malware scan: %s\n", scanner_name); + if (malware_daemon_ctx.sock < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); + break; + } - switch (scanent->scancode) + switch (scanent->scancode) + { +#ifndef DISABLE_MAL_FFROTD + case M_FPROTD: /* "f-protd" scanner type -------------------------------- */ { - case M_FPROTD: /* "f-protd" scanner type ------------------------------ */ - { - uschar *fp_scan_option; - unsigned int detected=0, par_count=0; - uschar * scanrequest; - uschar buf[32768], *strhelper, *strhelper2; - uschar * malware_name_internal = NULL; - int len; + uschar *fp_scan_option; + unsigned int detected=0, par_count=0; + uschar * scanrequest; + uschar buf[32768], *strhelper, *strhelper2; + uschar * malware_name_internal = NULL; + int len; - scanrequest = string_sprintf("GET %s", eml_filename); + scanrequest = string_sprintf("GET %s", eml_filename); - while ((fp_scan_option = string_nextinlist(&av_scanner_work, &sep, - NULL, 0))) - { - scanrequest = string_sprintf("%s%s%s", scanrequest, - par_count ? "%20" : "?", fp_scan_option); - par_count++; - } - scanrequest = string_sprintf("%s HTTP/1.0\r\n\r\n", scanrequest); - DEBUG(D_acl) debug_printf("Malware scan: issuing %s: %s\n", - scanner_name, scanrequest); + while ((fp_scan_option = string_nextinlist(&av_scanner_work, &sep, + NULL, 0))) + { + scanrequest = string_sprintf("%s%s%s", scanrequest, + par_count ? "%20" : "?", fp_scan_option); + par_count++; + } + scanrequest = string_sprintf("%s HTTP/1.0\r\n\r\n", scanrequest); + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s: %s\n", + scanner_name, scanrequest); - /* send scan request */ - if (m_sock_send(sock, scanrequest, Ustrlen(scanrequest)+1, &errstr) < 0) - return m_errlog_defer(scanent, errstr); + /* send scan request */ + if (m_sock_send(malware_daemon_ctx.sock, scanrequest, Ustrlen(scanrequest)+1, &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); - while ((len = recv_line(sock, buf, sizeof(buf), tmo)) >= 0) - if (len > 0) + while ((len = recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo)) >= 0) + if (len > 0) + { + if (Ustrstr(buf, US""))) { - if (Ustrstr(buf, US""))) - { - if ((strhelper2 = Ustrstr(buf, US"")) != NULL) - { - *strhelper2 = '\0'; - malware_name_internal = string_copy(strhelper+6); - } - } - else if (Ustrstr(buf, US"")) != NULL) { - malware_name = Ustrstr(buf, US"") - ? malware_name_internal : NULL; - break; + *strhelper2 = '\0'; + malware_name_internal = string_copy(strhelper+6); } } - break; - } /* f-protd */ - - case M_DRWEB: /* "drweb" scanner type --------------------------------- */ - /* v0.1 - added support for tcp sockets */ - /* v0.0 - initial release -- support for unix sockets */ - { - int result; - off_t fsize; - unsigned int fsize_uint; - uschar * tmpbuf, *drweb_fbuf; - int drweb_rc, drweb_cmd, drweb_flags = 0x0000, drweb_fd, - drweb_vnum, drweb_slen, drweb_fin = 0x0000; - const pcre *drweb_re; - - /* prepare variables */ - drweb_cmd = htonl(DRWEBD_SCAN_CMD); - drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL); - - if (*scanner_options != '/') - { - /* calc file size */ - if ((drweb_fd = open(CS eml_filename, O_RDONLY)) == -1) - return m_errlog_defer_3(scanent, - string_sprintf("can't open spool file %s: %s", - eml_filename, strerror(errno)), - sock); - - if ((fsize = lseek(drweb_fd, 0, SEEK_END)) == -1) + else if (Ustrstr(buf, US"") + ? malware_name_internal : NULL; + break; } - drweb_slen = htonl(fsize); - lseek(drweb_fd, 0, SEEK_SET); + } + if (len < -1) + { + (void)close(malware_daemon_ctx.sock); + return DEFER; + } + break; + } /* f-protd */ +#endif - DEBUG(D_acl) debug_printf("Malware scan: issuing %s remote scan [%s]\n", - scanner_name, scanner_options); +#ifndef DISABLE_MAL_FFROT6D + case M_FPROT6D: /* "f-prot6d" scanner type ----------------------------------- */ + { + int bread; + uschar * e; + uschar * linebuffer; + uschar * scanrequest; + uschar av_buffer[1024]; - /* send scan request */ - if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || - (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || - (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0) || - (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0)) - { - (void)close(drweb_fd); - return m_errlog_defer_3(scanent, string_sprintf( - "unable to send commands to socket (%s)", scanner_options), - sock); - } + if ((!fprot6d_re_virus && !(fprot6d_re_virus = m_pcre_compile(fprot6d_re_virus_str, &errstr))) + || (!fprot6d_re_error && !(fprot6d_re_error = m_pcre_compile(fprot6d_re_error_str, &errstr)))) + return malware_panic_defer(errstr); - if (!(drweb_fbuf = (uschar *) malloc (fsize_uint))) - { - (void)close(drweb_fd); - return m_errlog_defer_3(scanent, - string_sprintf("unable to allocate memory %u for file (%s)", - fsize_uint, eml_filename), - sock); - } + scanrequest = string_sprintf("SCAN FILE %s\n", eml_filename); + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s: %s\n", + scanner_name, scanrequest); - if ((result = read (drweb_fd, drweb_fbuf, fsize)) == -1) - { - int err = errno; - (void)close(drweb_fd); - free(drweb_fbuf); - return m_errlog_defer_3(scanent, - string_sprintf("can't read spool file %s: %s", - eml_filename, strerror(err)), - sock); - } - (void)close(drweb_fd); + if (m_sock_send(malware_daemon_ctx.sock, scanrequest, Ustrlen(scanrequest), &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); - /* send file body to socket */ - if (send(sock, drweb_fbuf, fsize, 0) < 0) - { - free(drweb_fbuf); - return m_errlog_defer_3(scanent, string_sprintf( - "unable to send file body to socket (%s)", scanner_options), - sock); - } - (void)close(drweb_fd); - } - else - { - drweb_slen = htonl(Ustrlen(eml_filename)); - - DEBUG(D_acl) debug_printf("Malware scan: issuing %s local scan [%s]\n", - scanner_name, scanner_options); - - /* send scan request */ - if ((send(sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || - (send(sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || - (send(sock, &drweb_slen, sizeof(drweb_slen), 0) < 0) || - (send(sock, eml_filename, Ustrlen(eml_filename), 0) < 0) || - (send(sock, &drweb_fin, sizeof(drweb_fin), 0) < 0)) - return m_errlog_defer_3(scanent, string_sprintf( - "unable to send commands to socket (%s)", scanner_options), - sock); - } + bread = ip_recv(&malware_daemon_ctx, av_buffer, sizeof(av_buffer), tmo); - /* wait for result */ - if (!recv_len(sock, &drweb_rc, sizeof(drweb_rc), tmo)) - return m_errlog_defer_3(scanent, - US"unable to read return code", sock); - drweb_rc = ntohl(drweb_rc); + if (bread <= 0) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to read from socket (%s)", strerror(errno)), + malware_daemon_ctx.sock); - if (!recv_len(sock, &drweb_vnum, sizeof(drweb_vnum), tmo)) - return m_errlog_defer_3(scanent, - US"unable to read the number of viruses", sock); - drweb_vnum = ntohl(drweb_vnum); + if (bread == sizeof(av_buffer)) + return m_panic_defer_3(scanent, CUS callout_address, + US"buffer too small", malware_daemon_ctx.sock); - /* "virus(es) found" if virus number is > 0 */ - if (drweb_vnum) - { - int i; + av_buffer[bread] = '\0'; + linebuffer = string_copy(av_buffer); - /* setup default virus name */ - malware_name = US"unknown"; + m_sock_send(malware_daemon_ctx.sock, US"QUIT\n", 5, 0); - /* set up match regex */ - drweb_re = m_pcre_compile(US"infected\\swith\\s*(.+?)$", &errstr); + if ((e = m_pcre_exec(fprot6d_re_error, linebuffer))) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("scanner reported error (%s)", e), malware_daemon_ctx.sock); - /* read and concatenate virus names into one string */ - for (i = 0; i < drweb_vnum; i++) - { - int size = 0, off = 0, ovector[10*3]; - /* read the size of report */ - if (!recv_len(sock, &drweb_slen, sizeof(drweb_slen), tmo)) - return m_errlog_defer_3(scanent, - US"cannot read report size", sock); - drweb_slen = ntohl(drweb_slen); - tmpbuf = store_get(drweb_slen); - - /* read report body */ - if (!recv_len(sock, tmpbuf, drweb_slen, tmo)) - return m_errlog_defer_3(scanent, - US"cannot read report string", sock); - tmpbuf[drweb_slen] = '\0'; - - /* try matcher on the line, grab substring */ - result = pcre_exec(drweb_re, NULL, CS tmpbuf, Ustrlen(tmpbuf), 0, 0, - ovector, nelements(ovector)); - if (result >= 2) - { - const char * pre_malware_nb; + if (!(malware_name = m_pcre_exec(fprot6d_re_virus, linebuffer))) + malware_name = NULL; - pcre_get_substring(CS tmpbuf, ovector, result, 1, &pre_malware_nb); + break; + } /* f-prot6d */ +#endif - if (i==0) /* the first name we just copy to malware_name */ - malware_name = string_append(NULL, &size, &off, - 1, pre_malware_nb); +#ifndef DISABLE_MAL_DRWEB + case M_DRWEB: /* "drweb" scanner type ----------------------------------- */ + /* v0.1 - added support for tcp sockets */ + /* v0.0 - initial release -- support for unix sockets */ + { + int result; + off_t fsize; + unsigned int fsize_uint; + uschar * tmpbuf, *drweb_fbuf; + int drweb_rc, drweb_cmd, drweb_flags = 0x0000, drweb_fd, + drweb_vnum, drweb_slen, drweb_fin = 0x0000; + + /* prepare variables */ + drweb_cmd = htonl(DRWEBD_SCAN_CMD); + drweb_flags = htonl(DRWEBD_RETURN_VIRUSES | DRWEBD_IS_MAIL); + + if (*scanner_options != '/') + { + /* calc file size */ + if ((drweb_fd = exim_open2(CCS eml_filename, O_RDONLY)) == -1) + return m_panic_defer_3(scanent, NULL, + string_sprintf("can't open spool file %s: %s", + eml_filename, strerror(errno)), + malware_daemon_ctx.sock); + + if ((fsize = lseek(drweb_fd, 0, SEEK_END)) == -1) + { + int err; +badseek: err = errno; + (void)close(drweb_fd); + return m_panic_defer_3(scanent, NULL, + string_sprintf("can't seek spool file %s: %s", + eml_filename, strerror(err)), + malware_daemon_ctx.sock); + } + fsize_uint = (unsigned int) fsize; + if ((off_t)fsize_uint != fsize) + { + (void)close(drweb_fd); + return m_panic_defer_3(scanent, NULL, + string_sprintf("seeking spool file %s, size overflow", + eml_filename), + malware_daemon_ctx.sock); + } + drweb_slen = htonl(fsize); + if (lseek(drweb_fd, 0, SEEK_SET) < 0) + goto badseek; - else /* concatenate each new virus name to previous */ - malware_name = string_append(malware_name, &size, &off, - 2, "/", pre_malware_nb); + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s remote scan [%s]\n", + scanner_name, scanner_options); - pcre_free_substring(pre_malware_nb); - } - } + /* send scan request */ + if ((send(malware_daemon_ctx.sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || + (send(malware_daemon_ctx.sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || + (send(malware_daemon_ctx.sock, &drweb_fin, sizeof(drweb_fin), 0) < 0) || + (send(malware_daemon_ctx.sock, &drweb_slen, sizeof(drweb_slen), 0) < 0)) + { + (void)close(drweb_fd); + return m_panic_defer_3(scanent, CUS callout_address, string_sprintf( + "unable to send commands to socket (%s)", scanner_options), + malware_daemon_ctx.sock); } - else + + if (!(drweb_fbuf = store_malloc(fsize_uint))) { - const char *drweb_s = NULL; - - if (drweb_rc & DERR_READ_ERR) drweb_s = "read error"; - if (drweb_rc & DERR_NOMEMORY) drweb_s = "no memory"; - if (drweb_rc & DERR_TIMEOUT) drweb_s = "timeout"; - if (drweb_rc & DERR_BAD_CALL) drweb_s = "wrong command"; - /* retcodes DERR_SYMLINK, DERR_NO_REGFILE, DERR_SKIPPED. - * DERR_TOO_BIG, DERR_TOO_COMPRESSED, DERR_SPAM, - * DERR_CRC_ERROR, DERR_READSOCKET, DERR_WRITE_ERR - * and others are ignored */ - if (drweb_s) - return m_errlog_defer_3(scanent, - string_sprintf("drweb daemon retcode 0x%x (%s)", drweb_rc, drweb_s), - sock); - - /* no virus found */ - malware_name = NULL; + (void)close(drweb_fd); + return m_panic_defer_3(scanent, NULL, + string_sprintf("unable to allocate memory %u for file (%s)", + fsize_uint, eml_filename), + malware_daemon_ctx.sock); } - break; - } /* drweb */ - case M_AVES: /* "aveserver" scanner type -------------------------------- */ - { - uschar buf[32768]; - int result; + if ((result = read (drweb_fd, drweb_fbuf, fsize)) == -1) + { + int err = errno; + (void)close(drweb_fd); + store_free(drweb_fbuf); + return m_panic_defer_3(scanent, NULL, + string_sprintf("can't read spool file %s: %s", + eml_filename, strerror(err)), + malware_daemon_ctx.sock); + } + (void)close(drweb_fd); + + /* send file body to socket */ + if (send(malware_daemon_ctx.sock, drweb_fbuf, fsize, 0) < 0) + { + store_free(drweb_fbuf); + return m_panic_defer_3(scanent, CUS callout_address, string_sprintf( + "unable to send file body to socket (%s)", scanner_options), + malware_daemon_ctx.sock); + } + store_free(drweb_fbuf); + } + else + { + drweb_slen = htonl(Ustrlen(eml_filename)); - /* read aveserver's greeting and see if it is ready (2xx greeting) */ - recv_line(sock, buf, sizeof(buf), tmo); + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s local scan [%s]\n", + scanner_name, scanner_options); - if (buf[0] != '2') /* aveserver is having problems */ - return m_errlog_defer_3(scanent, - string_sprintf("unavailable (Responded: %s).", - ((buf[0] != 0) ? buf : (uschar *)"nothing") ), - sock); + /* send scan request */ + if ((send(malware_daemon_ctx.sock, &drweb_cmd, sizeof(drweb_cmd), 0) < 0) || + (send(malware_daemon_ctx.sock, &drweb_flags, sizeof(drweb_flags), 0) < 0) || + (send(malware_daemon_ctx.sock, &drweb_slen, sizeof(drweb_slen), 0) < 0) || + (send(malware_daemon_ctx.sock, eml_filename, Ustrlen(eml_filename), 0) < 0) || + (send(malware_daemon_ctx.sock, &drweb_fin, sizeof(drweb_fin), 0) < 0)) + return m_panic_defer_3(scanent, CUS callout_address, string_sprintf( + "unable to send commands to socket (%s)", scanner_options), + malware_daemon_ctx.sock); + } - /* prepare our command */ - (void)string_format(buf, sizeof(buf), "SCAN bPQRSTUW %s\r\n", - eml_filename); + /* wait for result */ + if (!recv_len(malware_daemon_ctx.sock, &drweb_rc, sizeof(drweb_rc), tmo)) + return m_panic_defer_3(scanent, CUS callout_address, + US"unable to read return code", malware_daemon_ctx.sock); + drweb_rc = ntohl(drweb_rc); + + if (!recv_len(malware_daemon_ctx.sock, &drweb_vnum, sizeof(drweb_vnum), tmo)) + return m_panic_defer_3(scanent, CUS callout_address, + US"unable to read the number of viruses", malware_daemon_ctx.sock); + drweb_vnum = ntohl(drweb_vnum); + + /* "virus(es) found" if virus number is > 0 */ + if (drweb_vnum) + { + gstring * g = NULL; - DEBUG(D_acl) debug_printf("Malware scan: issuing %s SCAN\n", scanner_name); + /* setup default virus name */ + malware_name = US"unknown"; - /* and send it */ - if (m_sock_send(sock, buf, Ustrlen(buf), &errstr) < 0) - return m_errlog_defer(scanent, errstr); + /* set up match regex */ + if (!drweb_re) + drweb_re = m_pcre_compile(drweb_re_str, &errstr); - malware_name = NULL; - result = 0; - /* read response lines, find malware name and final response */ - while (recv_line(sock, buf, sizeof(buf), tmo) > 0) + /* read and concatenate virus names into one string */ + for (int i = 0; i < drweb_vnum; i++) { - debug_printf("aveserver: %s\n", buf); - if (buf[0] == '2') - break; - if (buf[0] == '5') /* aveserver is having problems */ - { - result = m_errlog_defer(scanent, - string_sprintf("unable to scan file %s (Responded: %s).", - eml_filename, buf)); - break; - } - if (Ustrncmp(buf,"322",3) == 0) + int ovector[10*3]; + + /* read the size of report */ + if (!recv_len(malware_daemon_ctx.sock, &drweb_slen, sizeof(drweb_slen), tmo)) + return m_panic_defer_3(scanent, CUS callout_address, + US"cannot read report size", malware_daemon_ctx.sock); + drweb_slen = ntohl(drweb_slen); + + /* assume tainted, since it is external input */ + tmpbuf = store_get(drweb_slen, TRUE); + + /* read report body */ + if (!recv_len(malware_daemon_ctx.sock, tmpbuf, drweb_slen, tmo)) + return m_panic_defer_3(scanent, CUS callout_address, + US"cannot read report string", malware_daemon_ctx.sock); + tmpbuf[drweb_slen] = '\0'; + + /* try matcher on the line, grab substring */ + result = pcre_exec(drweb_re, NULL, CS tmpbuf, Ustrlen(tmpbuf), 0, 0, + ovector, nelem(ovector)); + if (result >= 2) { - uschar *p = Ustrchr(&buf[4], ' '); - *p = '\0'; - malware_name = string_copy(&buf[4]); - } - } + const char * pre_malware_nb; - /* and send it */ - if (m_sock_send(sock, US"quit\r\n", 6, &errstr) < 0) - return m_errlog_defer(scanent, errstr); + pcre_get_substring(CS tmpbuf, ovector, result, 1, &pre_malware_nb); - /* read aveserver's greeting and see if it is ready (2xx greeting) */ - recv_line(sock, buf, sizeof(buf), tmo); + if (i==0) /* the first name we just copy to malware_name */ + g = string_cat(NULL, US pre_malware_nb); - if (buf[0] != '2') /* aveserver is having problems */ - return m_errlog_defer_3(scanent, - string_sprintf("unable to quit dialogue (Responded: %s).", - ((buf[0] != 0) ? buf : (uschar *)"nothing") ), - sock); + /*XXX could be string_append_listele? */ + else /* concatenate each new virus name to previous */ + g = string_append(g, 2, "/", pre_malware_nb); - if (result == DEFER) + pcre_free_substring(pre_malware_nb); + } + } + malware_name = string_from_gstring(g); + } + else + { + const char *drweb_s = NULL; + + if (drweb_rc & DERR_READ_ERR) drweb_s = "read error"; + if (drweb_rc & DERR_NOMEMORY) drweb_s = "no memory"; + if (drweb_rc & DERR_TIMEOUT) drweb_s = "timeout"; + if (drweb_rc & DERR_BAD_CALL) drweb_s = "wrong command"; + /* retcodes DERR_SYMLINK, DERR_NO_REGFILE, DERR_SKIPPED. + * DERR_TOO_BIG, DERR_TOO_COMPRESSED, DERR_SPAM, + * DERR_CRC_ERROR, DERR_READSOCKET, DERR_WRITE_ERR + * and others are ignored */ + if (drweb_s) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("drweb daemon retcode 0x%x (%s)", drweb_rc, drweb_s), + malware_daemon_ctx.sock); + + /* no virus found */ + malware_name = NULL; + } + break; + } /* drweb */ +#endif + +#ifndef DISABLE_MAL_AVE + case M_AVES: /* "aveserver" scanner type -------------------------------- */ + { + uschar buf[32768]; + int result; + + /* read aveserver's greeting and see if it is ready (2xx greeting) */ + buf[0] = 0; + recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo); + + if (buf[0] != '2') /* aveserver is having problems */ + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unavailable (Responded: %s).", + ((buf[0] != 0) ? buf : US "nothing") ), + malware_daemon_ctx.sock); + + /* prepare our command */ + (void)string_format(buf, sizeof(buf), "SCAN bPQRSTUW %s\r\n", + eml_filename); + + /* and send it */ + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s %s\n", + scanner_name, buf); + if (m_sock_send(malware_daemon_ctx.sock, buf, Ustrlen(buf), &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); + + malware_name = NULL; + result = 0; + /* read response lines, find malware name and final response */ + while (recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo) > 0) + { + if (buf[0] == '2') + break; + if (buf[0] == '5') /* aveserver is having problems */ { - (void)close(sock); - return DEFER; + result = m_panic_defer(scanent, CUS callout_address, + string_sprintf("unable to scan file %s (Responded: %s).", + eml_filename, buf)); + break; } - break; + if (Ustrncmp(buf,"322",3) == 0) + { + uschar *p = Ustrchr(&buf[4], ' '); + *p = '\0'; + malware_name = string_copy(&buf[4]); + } + } + + if (m_sock_send(malware_daemon_ctx.sock, US"quit\r\n", 6, &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); + + /* read aveserver's greeting and see if it is ready (2xx greeting) */ + buf[0] = 0; + recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo); + + if (buf[0] != '2') /* aveserver is having problems */ + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to quit dialogue (Responded: %s).", + ((buf[0] != 0) ? buf : US "nothing") ), + malware_daemon_ctx.sock); + + if (result == DEFER) + { + (void)close(malware_daemon_ctx.sock); + return DEFER; + } + break; } /* aveserver */ +#endif +#ifndef DISABLE_MAL_FSECURE case M_FSEC: /* "fsecure" scanner type ---------------------------------- */ { - int i, j, bread = 0; - uschar * file_name; - uschar av_buffer[1024]; - const pcre * fs_inf; - static uschar *cmdopt[] = { US"CONFIGURE\tARCHIVE\t1\n", - US"CONFIGURE\tTIMEOUT\t0\n", - US"CONFIGURE\tMAXARCH\t5\n", - US"CONFIGURE\tMIME\t1\n" }; - - malware_name = NULL; - - DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n", - scanner_name, scanner_options); - /* pass options */ - memset(av_buffer, 0, sizeof(av_buffer)); - for (i = 0; i != nelements(cmdopt); i++) - { + int i, bread = 0; + uschar * file_name; + uschar av_buffer[1024]; + static uschar *cmdopt[] = { US"CONFIGURE\tARCHIVE\t1\n", + US"CONFIGURE\tTIMEOUT\t0\n", + US"CONFIGURE\tMAXARCH\t5\n", + US"CONFIGURE\tMIME\t1\n" }; + + malware_name = NULL; + + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n", + scanner_name, scanner_options); + /* pass options */ + memset(av_buffer, 0, sizeof(av_buffer)); + for (i = 0; i != nelem(cmdopt); i++) + { - if (m_sock_send(sock, cmdopt[i], Ustrlen(cmdopt[i]), &errstr) < 0) - return m_errlog_defer(scanent, errstr); - - bread = ip_recv(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL)); - if (bread > 0) av_buffer[bread]='\0'; - if (bread < 0) - return m_errlog_defer_3(scanent, - string_sprintf("unable to read answer %d (%s)", i, strerror(errno)), - sock); - for (j = 0; j < bread; j++) - if (av_buffer[j] == '\r' || av_buffer[j] == '\n') - av_buffer[j] ='@'; - } + if (m_sock_send(malware_daemon_ctx.sock, cmdopt[i], Ustrlen(cmdopt[i]), &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); + + bread = ip_recv(&malware_daemon_ctx, av_buffer, sizeof(av_buffer), tmo); + if (bread > 0) av_buffer[bread]='\0'; + if (bread < 0) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to read answer %d (%s)", i, strerror(errno)), + malware_daemon_ctx.sock); + for (int j = 0; j < bread; j++) + if (av_buffer[j] == '\r' || av_buffer[j] == '\n') + av_buffer[j] ='@'; + } - /* pass the mailfile to fsecure */ - file_name = string_sprintf("SCAN\t%s\n", eml_filename); + /* pass the mailfile to fsecure */ + file_name = string_sprintf("SCAN\t%s\n", eml_filename); - if (m_sock_send(sock, file_name, Ustrlen(file_name), &errstr) < 0) - return m_errlog_defer(scanent, errstr); + if (m_sock_send(malware_daemon_ctx.sock, file_name, Ustrlen(file_name), &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); - /* set up match */ - /* todo also SUSPICION\t */ - fs_inf = m_pcre_compile(US"\\S{0,5}INFECTED\\t[^\\t]*\\t([^\\t]+)\\t\\S*$", &errstr); + /* set up match */ + /* todo also SUSPICION\t */ + if (!fsec_re) + fsec_re = m_pcre_compile(fsec_re_str, &errstr); - /* read report, linewise. Apply a timeout as the Fsecure daemon - sometimes wants an answer to "PING" but they won't tell us what */ + /* read report, linewise. Apply a timeout as the Fsecure daemon + sometimes wants an answer to "PING" but they won't tell us what */ { - uschar * p = av_buffer; - uschar * q; + uschar * p = av_buffer; + uschar * q; - for (;;) + for (;;) + { + errno = ETIMEDOUT; + i = av_buffer+sizeof(av_buffer)-p; + if ((bread= ip_recv(&malware_daemon_ctx, p, i-1, tmo)) < 0) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to read result (%s)", strerror(errno)), + malware_daemon_ctx.sock); + + for (p[bread] = '\0'; (q = Ustrchr(p, '\n')); p = q+1) { - errno = ETIME; - i = av_buffer+sizeof(av_buffer)-p; - if ((bread= ip_recv(sock, p, i-1, tmo-time(NULL))) < 0) - return m_errlog_defer_3(scanent, - string_sprintf("unable to read result (%s)", strerror(errno)), - sock); - - for (p[bread] = '\0'; q = strchr(p, '\n'); p = q+1) - { - *q = '\0'; + *q = '\0'; - /* Really search for virus again? */ - if (!malware_name) - /* try matcher on the line, grab substring */ - malware_name = m_pcre_exec(fs_inf, p); + /* Really search for virus again? */ + if (!malware_name) + /* try matcher on the line, grab substring */ + malware_name = m_pcre_exec(fsec_re, p); - if (Ustrstr(p, "OK\tScan ok.")) - goto fsec_found; - } + if (Ustrstr(p, "OK\tScan ok.")) + goto fsec_found; + } - /* copy down the trailing partial line then read another chunk */ - i = av_buffer+sizeof(av_buffer)-p; - memmove(av_buffer, p, i); - p = av_buffer+i; + /* copy down the trailing partial line then read another chunk */ + i = av_buffer+sizeof(av_buffer)-p; + memmove(av_buffer, p, i); + p = av_buffer+i; } } fsec_found: break; } /* fsecure */ +#endif +#ifndef DISABLE_MAL_KAV case M_KAVD: /* "kavdaemon" scanner type -------------------------------- */ { - time_t t; - uschar tmpbuf[1024]; - uschar * scanrequest; - int kav_rc; - unsigned long kav_reportlen, bread; - const pcre *kav_re; - uschar *p; - - /* get current date and time, build scan request */ - time(&t); - /* pdp note: before the eml_filename parameter, this scanned the - directory; not finding documentation, so we'll strip off the directory. - The side-effect is that the test framework scanning may end up in - scanning more than was requested, but for the normal interface, this is - fine. */ - - strftime(CS tmpbuf, sizeof(tmpbuf), "%d %b %H:%M:%S", localtime(&t)); - scanrequest = string_sprintf("<0>%s:%s", CS tmpbuf, eml_filename); - p = Ustrrchr(scanrequest, '/'); - if (p) - *p = '\0'; + time_t t; + uschar tmpbuf[1024]; + uschar * scanrequest; + int kav_rc; + unsigned long kav_reportlen; + int bread; + const pcre *kav_re; + uschar *p; + + /* get current date and time, build scan request */ + time(&t); + /* pdp note: before the eml_filename parameter, this scanned the + directory; not finding documentation, so we'll strip off the directory. + The side-effect is that the test framework scanning may end up in + scanning more than was requested, but for the normal interface, this is + fine. */ + + strftime(CS tmpbuf, sizeof(tmpbuf), "%d %b %H:%M:%S", localtime(&t)); + scanrequest = string_sprintf("<0>%s:%s", CS tmpbuf, eml_filename); + p = Ustrrchr(scanrequest, '/'); + if (p) + *p = '\0'; + + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n", + scanner_name, scanner_options); + + /* send scan request */ + if (m_sock_send(malware_daemon_ctx.sock, scanrequest, Ustrlen(scanrequest)+1, &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); - DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n", - scanner_name, scanner_options); + /* wait for result */ + if (!recv_len(malware_daemon_ctx.sock, tmpbuf, 2, tmo)) + return m_panic_defer_3(scanent, CUS callout_address, + US"unable to read 2 bytes from socket.", malware_daemon_ctx.sock); - /* send scan request */ - if (m_sock_send(sock, scanrequest, Ustrlen(scanrequest)+1, &errstr) < 0) - return m_errlog_defer(scanent, errstr); + /* get errorcode from one nibble */ + kav_rc = tmpbuf[ test_byte_order()==LITTLE_MY_ENDIAN ? 0 : 1 ] & 0x0F; + switch(kav_rc) + { + case 5: case 6: /* improper kavdaemon configuration */ + return m_panic_defer_3(scanent, CUS callout_address, + US"please reconfigure kavdaemon to NOT disinfect or remove infected files.", + malware_daemon_ctx.sock); + case 1: + return m_panic_defer_3(scanent, CUS callout_address, + US"reported 'scanning not completed' (code 1).", malware_daemon_ctx.sock); + case 7: + return m_panic_defer_3(scanent, CUS callout_address, + US"reported 'kavdaemon damaged' (code 7).", malware_daemon_ctx.sock); + } - /* wait for result */ - if (!recv_len(sock, tmpbuf, 2, tmo)) - return m_errlog_defer_3(scanent, - US"unable to read 2 bytes from socket.", sock); + /* code 8 is not handled, since it is ambiguous. It appears mostly on + bounces where part of a file has been cut off */ - /* get errorcode from one nibble */ - kav_rc = tmpbuf[ test_byte_order()==LITTLE_MY_ENDIAN ? 0 : 1 ] & 0x0F; - switch(kav_rc) + /* "virus found" return codes (2-4) */ + if (kav_rc > 1 && kav_rc < 5) { - case 5: case 6: /* improper kavdaemon configuration */ - return m_errlog_defer_3(scanent, - US"please reconfigure kavdaemon to NOT disinfect or remove infected files.", - sock); - case 1: - return m_errlog_defer_3(scanent, - US"reported 'scanning not completed' (code 1).", sock); - case 7: - return m_errlog_defer_3(scanent, - US"reported 'kavdaemon damaged' (code 7).", sock); - } - - /* code 8 is not handled, since it is ambigous. It appears mostly on - bounces where part of a file has been cut off */ + int report_flag = 0; - /* "virus found" return codes (2-4) */ - if (kav_rc > 1 && kav_rc < 5) - { - int report_flag = 0; + /* setup default virus name */ + malware_name = US"unknown"; - /* setup default virus name */ - malware_name = US"unknown"; + report_flag = tmpbuf[ test_byte_order() == LITTLE_MY_ENDIAN ? 1 : 0 ]; - report_flag = tmpbuf[ test_byte_order() == LITTLE_MY_ENDIAN ? 1 : 0 ]; - - /* read the report, if available */ - if (report_flag == 1) + /* read the report, if available */ + if (report_flag == 1) + { + /* read report size */ + if (!recv_len(malware_daemon_ctx.sock, &kav_reportlen, 4, tmo)) + return m_panic_defer_3(scanent, CUS callout_address, + US"cannot read report size", malware_daemon_ctx.sock); + + /* it's possible that avp returns av_buffer[1] == 1 but the + reportsize is 0 (!?) */ + if (kav_reportlen > 0) { - /* read report size */ - if (!recv_len(sock, &kav_reportlen, 4, tmo)) - return m_errlog_defer_3(scanent, - US"cannot read report size", sock); - - /* it's possible that avp returns av_buffer[1] == 1 but the - reportsize is 0 (!?) */ - if (kav_reportlen > 0) + /* set up match regex, depends on retcode */ + if (kav_rc == 3) { - /* set up match regex, depends on retcode */ - kav_re = m_pcre_compile( kav_rc == 3 - ? US"suspicion:\\s*(.+?)\\s*$" - : US"infected:\\s*(.+?)\\s*$", - &errstr ); - - /* read report, linewise */ - while (kav_reportlen > 0) - { - if ((bread = recv_line(sock, tmpbuf, sizeof(tmpbuf), tmo)) < 0) - break; - kav_reportlen -= bread+1; - - /* try matcher on the line, grab substring */ - if ((malware_name = m_pcre_exec(kav_re, tmpbuf))) - break; - } + if (!kav_re_sus) kav_re_sus = m_pcre_compile(kav_re_sus_str, &errstr); + kav_re = kav_re_sus; + } + else + { + if (!kav_re_inf) kav_re_inf = m_pcre_compile(kav_re_inf_str, &errstr); + kav_re = kav_re_inf; + } + + /* read report, linewise. Using size from stream to read amount of data + from same stream is safe enough. */ + /* coverity[tainted_data] */ + while (kav_reportlen > 0) + { + if ((bread = recv_line(malware_daemon_ctx.sock, tmpbuf, sizeof(tmpbuf), tmo)) < 0) + break; + kav_reportlen -= bread+1; + + /* try matcher on the line, grab substring */ + if ((malware_name = m_pcre_exec(kav_re, tmpbuf))) + break; } } } - else /* no virus found */ - malware_name = NULL; + } + else /* no virus found */ + malware_name = NULL; - break; + break; } +#endif +#ifndef DISABLE_MAL_CMDLINE case M_CMDL: /* "cmdline" scanner type ---------------------------------- */ { - const uschar *cmdline_scanner = scanner_options; - const pcre *cmdline_trigger_re; - const pcre *cmdline_regex_re; - uschar * file_name; - uschar * commandline; - void (*eximsigchld)(int); - void (*eximsigpipe)(int); - FILE *scanner_out = NULL; - int scanner_fd; - FILE *scanner_record = NULL; - uschar linebuffer[32767]; - int rcnt; - int trigger = 0; - uschar *p; - - if (!cmdline_scanner) - return m_errlog_defer(scanent, errstr); - - /* find scanner output trigger */ - cmdline_trigger_re = m_pcre_nextinlist(&av_scanner_work, &sep, - "missing trigger specification", &errstr); - if (!cmdline_trigger_re) - return m_errlog_defer(scanent, errstr); - - /* find scanner name regex */ - cmdline_regex_re = m_pcre_nextinlist(&av_scanner_work, &sep, - "missing virus name regex specification", &errstr); - if (!cmdline_regex_re) - return m_errlog_defer(scanent, errstr); - - /* prepare scanner call; despite the naming, file_name holds a directory - name which is documented as the value given to %s. */ - - file_name = string_copy(eml_filename); - p = Ustrrchr(file_name, '/'); - if (p) - *p = '\0'; - commandline = string_sprintf(CS cmdline_scanner, file_name); - - /* redirect STDERR too */ - commandline = string_sprintf("%s 2>&1", commandline); + const uschar *cmdline_scanner = scanner_options; + const pcre *cmdline_trigger_re; + const pcre *cmdline_regex_re; + uschar * file_name; + uschar * commandline; + void (*eximsigchld)(int); + void (*eximsigpipe)(int); + FILE *scanner_out = NULL; + int scanner_fd; + FILE *scanner_record = NULL; + uschar linebuffer[32767]; + int rcnt; + int trigger = 0; + uschar *p; + + if (!cmdline_scanner) + return m_panic_defer(scanent, NULL, errstr); + + /* find scanner output trigger */ + cmdline_trigger_re = m_pcre_nextinlist(&av_scanner_work, &sep, + "missing trigger specification", &errstr); + if (!cmdline_trigger_re) + return m_panic_defer(scanent, NULL, errstr); + + /* find scanner name regex */ + cmdline_regex_re = m_pcre_nextinlist(&av_scanner_work, &sep, + "missing virus name regex specification", &errstr); + if (!cmdline_regex_re) + return m_panic_defer(scanent, NULL, errstr); + + /* prepare scanner call; despite the naming, file_name holds a directory + name which is documented as the value given to %s. */ + + file_name = string_copy(eml_filename); + p = Ustrrchr(file_name, '/'); + if (p) + *p = '\0'; + commandline = string_sprintf(CS cmdline_scanner, file_name); + + /* redirect STDERR too */ + commandline = string_sprintf("%s 2>&1", commandline); + + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n", + scanner_name, commandline); + + /* store exims signal handlers */ + eximsigchld = signal(SIGCHLD,SIG_DFL); + eximsigpipe = signal(SIGPIPE,SIG_DFL); + + if (!(scanner_out = popen(CS commandline,"r"))) + { + int err = errno; + signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); + return m_panic_defer(scanent, NULL, + string_sprintf("call (%s) failed: %s.", commandline, strerror(err))); + } + scanner_fd = fileno(scanner_out); - DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n", - scanner_name, commandline); + file_name = string_sprintf("%s/%s_scanner_output", eml_dir, message_id); - /* store exims signal handlers */ - eximsigchld = signal(SIGCHLD,SIG_DFL); - eximsigpipe = signal(SIGPIPE,SIG_DFL); + if (!(scanner_record = modefopen(file_name, "wb", SPOOL_MODE))) + { + int err = errno; + (void) pclose(scanner_out); + signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); + return m_panic_defer(scanent, NULL, string_sprintf( + "opening scanner output file (%s) failed: %s.", + file_name, strerror(err))); + } - if (!(scanner_out = popen(CS commandline,"r"))) + /* look for trigger while recording output */ + while ((rcnt = recv_line(scanner_fd, linebuffer, + sizeof(linebuffer), tmo))) + { + if (rcnt < 0) { int err = errno; + if (rcnt == -1) + break; + (void) pclose(scanner_out); signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); - return m_errlog_defer(scanent, - string_sprintf("call (%s) failed: %s.", commandline, strerror(err))); + return m_panic_defer(scanent, NULL, string_sprintf( + "unable to read from scanner (%s): %s", + commandline, strerror(err))); } - scanner_fd = fileno(scanner_out); - file_name = string_sprintf("%s/scan/%s/%s_scanner_output", - spool_directory, message_id, message_id); - - if (!(scanner_record = modefopen(file_name, "wb", SPOOL_MODE))) + if (Ustrlen(linebuffer) > fwrite(linebuffer, 1, Ustrlen(linebuffer), scanner_record)) { - int err = errno; + /* short write */ (void) pclose(scanner_out); signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); - return m_errlog_defer(scanent, string_sprintf( - "opening scanner output file (%s) failed: %s.", - file_name, strerror(err))); + return m_panic_defer(scanent, NULL, string_sprintf( + "short write on scanner output file (%s).", file_name)); } + putc('\n', scanner_record); + /* try trigger match */ + if ( !trigger + && regex_match_and_setup(cmdline_trigger_re, linebuffer, 0, -1) + ) + trigger = 1; + } - /* look for trigger while recording output */ - while ((rcnt = recv_line(scanner_fd, linebuffer, - sizeof(linebuffer), tmo))) - { - if (rcnt < 0) - { - (void) pclose(scanner_out); - signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); - return m_errlog_defer(scanent, string_sprintf( - "unable to read from scanner (%s)", scanner_options)); - } - - if (Ustrlen(linebuffer) > fwrite(linebuffer, 1, Ustrlen(linebuffer), scanner_record)) - { - /* short write */ - (void) pclose(scanner_out); - signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); - return m_errlog_defer(scanent, string_sprintf( - "short write on scanner output file (%s).", file_name)); - } - putc('\n', scanner_record); - /* try trigger match */ - if ( !trigger - && regex_match_and_setup(cmdline_trigger_re, linebuffer, 0, -1) - ) - trigger = 1; - } + (void)fclose(scanner_record); + sep = pclose(scanner_out); + signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); + if (sep != 0) + return m_panic_defer(scanent, NULL, + sep == -1 + ? string_sprintf("running scanner failed: %s", strerror(sep)) + : string_sprintf("scanner returned error code: %d", sep)); + if (trigger) + { + uschar * s; + /* setup default virus name */ + malware_name = US"unknown"; + + /* re-open the scanner output file, look for name match */ + scanner_record = Ufopen(file_name, "rb"); + while (Ufgets(linebuffer, sizeof(linebuffer), scanner_record)) + if ((s = m_pcre_exec(cmdline_regex_re, linebuffer))) /* try match */ + malware_name = s; (void)fclose(scanner_record); - sep = pclose(scanner_out); - signal(SIGCHLD,eximsigchld); signal(SIGPIPE,eximsigpipe); - if (sep != 0) - return m_errlog_defer(scanent, - sep == -1 - ? string_sprintf("running scanner failed: %s", strerror(sep)) - : string_sprintf("scanner returned error code: %d", sep)); - - if (trigger) - { - uschar * s; - /* setup default virus name */ - malware_name = US"unknown"; - - /* re-open the scanner output file, look for name match */ - scanner_record = fopen(CS file_name, "rb"); - while (fgets(CS linebuffer, sizeof(linebuffer), scanner_record)) - { - /* try match */ - if ((s = m_pcre_exec(cmdline_regex_re, linebuffer))) - malware_name = s; - } - (void)fclose(scanner_record); - } - else /* no virus found */ - malware_name = NULL; - break; + } + else /* no virus found */ + malware_name = NULL; + break; } /* cmdline */ +#endif +#ifndef DISABLE_MAL_SOPHIE case M_SOPHIE: /* "sophie" scanner type --------------------------------- */ { - int bread = 0; - uschar *p; - uschar * file_name; - uschar av_buffer[1024]; - - /* pass the scan directory to sophie */ - file_name = string_copy(eml_filename); - if ((p = Ustrrchr(file_name, '/'))) - *p = '\0'; + int bread = 0; + uschar *p; + uschar * file_name; + uschar av_buffer[1024]; - DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan [%s]\n", - scanner_name, scanner_options); + /* pass the scan directory to sophie */ + file_name = string_copy(eml_filename); + if ((p = Ustrrchr(file_name, '/'))) + *p = '\0'; - if ( write(sock, file_name, Ustrlen(file_name)) < 0 - || write(sock, "\n", 1) != 1 - ) - return m_errlog_defer_3(scanent, - string_sprintf("unable to write to UNIX socket (%s)", scanner_options), - sock); - - /* wait for result */ - memset(av_buffer, 0, sizeof(av_buffer)); - if ((bread = ip_recv(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL))) <= 0) - return m_errlog_defer_3(scanent, - string_sprintf("unable to read from UNIX socket (%s)", scanner_options), - sock); - - /* infected ? */ - if (av_buffer[0] == '1') { - uschar * s = Ustrchr(av_buffer, '\n'); - if (s) - *s = '\0'; - malware_name = string_copy(&av_buffer[2]); - } - else if (!strncmp(CS av_buffer, "-1", 2)) - return m_errlog_defer_3(scanent, US"scanner reported error", sock); - else /* all ok, no virus */ - malware_name = NULL; + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n", + scanner_name, scanner_options); - break; + if ( write(malware_daemon_ctx.sock, file_name, Ustrlen(file_name)) < 0 + || write(malware_daemon_ctx.sock, "\n", 1) != 1 + ) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to write to UNIX socket (%s)", scanner_options), + malware_daemon_ctx.sock); + + /* wait for result */ + memset(av_buffer, 0, sizeof(av_buffer)); + if ((bread = ip_recv(&malware_daemon_ctx, av_buffer, sizeof(av_buffer), tmo)) <= 0) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to read from UNIX socket (%s)", scanner_options), + malware_daemon_ctx.sock); + + /* infected ? */ + if (av_buffer[0] == '1') { + uschar * s = Ustrchr(av_buffer, '\n'); + if (s) + *s = '\0'; + malware_name = string_copy(&av_buffer[2]); + } + else if (!strncmp(CS av_buffer, "-1", 2)) + return m_panic_defer_3(scanent, CUS callout_address, + US"scanner reported error", malware_daemon_ctx.sock); + else /* all ok, no virus */ + malware_name = NULL; + + break; } +#endif +#ifndef DISABLE_MAL_CLAM case M_CLAMD: /* "clamd" scanner type ----------------------------------- */ { - /* This code was originally contributed by David Saez */ - /* There are three scanning methods available to us: - * (1) Use the SCAN command, pointing to a file in the filesystem - * (2) Use the STREAM command, send the data on a separate port - * (3) Use the zINSTREAM command, send the data inline - * The zINSTREAM command was introduced with ClamAV 0.95, which marked - * STREAM deprecated; see: http://wiki.clamav.net/bin/view/Main/UpgradeNotes095 - * In Exim, we use SCAN if using a Unix-domain socket or explicitly told that - * the TCP-connected daemon is actually local; otherwise we use zINSTREAM unless - * WITH_OLD_CLAMAV_STREAM is defined. - * See Exim bug 926 for details. */ - - uschar *p, *vname, *result_tag, *response_end; - int bread=0; - uschar * file_name; - uschar av_buffer[1024]; - uschar *hostname = US""; - host_item connhost; - uschar *clamav_fbuf; - int clam_fd, result; - off_t fsize; - unsigned int fsize_uint; - BOOL use_scan_command = FALSE; - clamd_address_container * clamd_address_vector[MAX_CLAMD_SERVERS]; - int current_server; - int num_servers = 0; - #ifdef WITH_OLD_CLAMAV_STREAM - unsigned int port; - uschar av_buffer2[1024]; - int sockData; - #else - uint32_t send_size, send_final_zeroblock; - #endif - - if (*scanner_options == '/') - /* Local file; so we def want to use_scan_command and don't want to try - * passing IP/port combinations */ - use_scan_command = TRUE; - else +/* This code was originally contributed by David Saez */ +/* There are three scanning methods available to us: +* (1) Use the SCAN command, pointing to a file in the filesystem +* (2) Use the STREAM command, send the data on a separate port +* (3) Use the zINSTREAM command, send the data inline +* The zINSTREAM command was introduced with ClamAV 0.95, which marked +* STREAM deprecated; see: http://wiki.clamav.net/bin/view/Main/UpgradeNotes095 +* In Exim, we use SCAN if using a Unix-domain socket or explicitly told that +* the TCP-connected daemon is actually local; otherwise we use zINSTREAM +* See Exim bug 926 for details. */ + + uschar *p, *vname, *result_tag; + int bread=0; + uschar av_buffer[1024]; + uschar *hostname = US""; + host_item connhost; + uschar *clamav_fbuf; + int clam_fd, result; + off_t fsize; + unsigned int fsize_uint; + BOOL use_scan_command = FALSE; + clamd_address * cv[MAX_CLAMD_SERVERS]; + int num_servers = 0; + uint32_t send_size, send_final_zeroblock; + blob cmd_str; + + /*XXX if unixdomain socket, only one server supported. Needs fixing; + there's no reason we should not mix local and remote servers */ + + if (*scanner_options == '/') + { + clamd_address * cd; + const uschar * sublist; + int subsep = ' '; + + /* Local file; so we def want to use_scan_command and don't want to try + * passing IP/port combinations */ + use_scan_command = TRUE; + cd = (clamd_address *) store_get(sizeof(clamd_address), FALSE); + + /* extract socket-path part */ + sublist = scanner_options; + cd->hostspec = string_nextinlist(&sublist, &subsep, NULL, 0); + + /* parse options */ + if (clamd_option(cd, sublist, &subsep) != OK) + return m_panic_defer(scanent, NULL, + string_sprintf("bad option '%s'", scanner_options)); + cv[0] = cd; + } + else + { + /* Go through the rest of the list of host/port and construct an array + * of servers to try. The first one is the bit we just passed from + * scanner_options so process that first and then scan the remainder of + * the address buffer */ + do { - const uschar *address = scanner_options; - uschar address_buffer[MAX_CLAMD_ADDRESS_LENGTH + 20]; - - /* Go through the rest of the list of host/port and construct an array - * of servers to try. The first one is the bit we just passed from - * scanner_options so process that first and then scan the remainder of - * the address buffer */ - do + clamd_address * cd; + const uschar * sublist; + int subsep = ' '; + uschar * s; + + /* The 'local' option means use the SCAN command over the network + * socket (ie common file storage in use) */ + /*XXX we could accept this also as a local option? */ + if (strcmpic(scanner_options, US"local") == 0) { - clamd_address_container *this_clamd; + use_scan_command = TRUE; + continue; + } - /* The 'local' option means use the SCAN command over the network - * socket (ie common file storage in use) */ - if (strcmpic(address,US"local") == 0) - { - use_scan_command = TRUE; - continue; - } + cd = (clamd_address *) store_get(sizeof(clamd_address), FALSE); - /* XXX: If unsuccessful we should free this memory */ - this_clamd = - (clamd_address_container *)store_get(sizeof(clamd_address_container)); + /* extract host and port part */ + sublist = scanner_options; + if (!(cd->hostspec = string_nextinlist(&sublist, &subsep, NULL, 0))) + { + (void) m_panic_defer(scanent, NULL, + string_sprintf("missing address: '%s'", scanner_options)); + continue; + } + if (!(s = string_nextinlist(&sublist, &subsep, NULL, 0))) + { + (void) m_panic_defer(scanent, NULL, + string_sprintf("missing port: '%s'", scanner_options)); + continue; + } + cd->tcp_port = atoi(CS s); - /* extract host and port part */ - if( sscanf(CS address, "%" MAX_CLAMD_ADDRESS_LENGTH_S "s %u", - this_clamd->tcp_addr, &(this_clamd->tcp_port)) != 2 ) - { - (void) m_errlog_defer(scanent, - string_sprintf("invalid address '%s'", address)); - continue; - } + /* parse options */ + /*XXX should these options be common over scanner types? */ + if (clamd_option(cd, sublist, &subsep) != OK) + return m_panic_defer(scanent, NULL, + string_sprintf("bad option '%s'", scanner_options)); - clamd_address_vector[num_servers] = this_clamd; - num_servers++; - if (num_servers >= MAX_CLAMD_SERVERS) - { - (void) m_errlog_defer(scanent, - US"More than " MAX_CLAMD_SERVERS_S " clamd servers " - "specified; only using the first " MAX_CLAMD_SERVERS_S ); - break; - } - } while ((address = string_nextinlist(&av_scanner_work, &sep, - address_buffer, - sizeof(address_buffer))) != NULL); - - /* check if we have at least one server */ - if (!num_servers) - return m_errlog_defer(scanent, - US"no useable server addresses in malware configuration option."); - } + cv[num_servers++] = cd; + if (num_servers >= MAX_CLAMD_SERVERS) + { + (void) m_panic_defer(scanent, NULL, + US"More than " MAX_CLAMD_SERVERS_S " clamd servers " + "specified; only using the first " MAX_CLAMD_SERVERS_S ); + break; + } + } while ((scanner_options = string_nextinlist(&av_scanner_work, &sep, + NULL, 0))); - /* See the discussion of response formats below to see why we really - don't like colons in filenames when passing filenames to ClamAV. */ - if (use_scan_command && Ustrchr(eml_filename, ':')) - return m_errlog_defer(scanent, - string_sprintf("local/SCAN mode incompatible with" \ - " : in path to email filename [%s]", eml_filename)); + /* check if we have at least one server */ + if (!num_servers) + return m_panic_defer(scanent, NULL, + US"no useable server addresses in malware configuration option."); + } + + /* See the discussion of response formats below to see why we really + don't like colons in filenames when passing filenames to ClamAV. */ + if (use_scan_command && Ustrchr(eml_filename, ':')) + return m_panic_defer(scanent, NULL, + string_sprintf("local/SCAN mode incompatible with" \ + " : in path to email filename [%s]", eml_filename)); + + /* Set up the very first data we will be sending */ + if (!use_scan_command) + { 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); + } - /* We have some network servers specified */ - if (num_servers) + /* 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. */ + + while (num_servers > 0) { - /* 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. */ + int i = random_number(num_servers); + clamd_address * cd = cv[i]; - while (num_servers > 0) + DEBUG(D_acl) debug_printf_indent("trying server name %s, port %u\n", + 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) */ + for (;;) { - int i; - /* Randomly pick a server to start with */ - current_server = random_number( num_servers ); - - debug_printf("trying server name %s, port %u\n", - clamd_address_vector[current_server]->tcp_addr, - clamd_address_vector[current_server]->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) */ - sock= m_tcpsocket(clamd_address_vector[current_server]->tcp_addr, - clamd_address_vector[current_server]->tcp_port, - &connhost, &errstr); - if (sock >= 0) + /*XXX we trust that the cmd_str is ideempotent */ + if ((malware_daemon_ctx.sock = m_tcpsocket(cd->hostspec, cd->tcp_port, + &connhost, &errstr, &cmd_str)) >= 0) { /* Connection successfully established with a server */ - hostname = clamd_address_vector[current_server]->tcp_addr; + hostname = cd->hostspec; + cmd_str.len = 0; break; } - - (void) m_errlog_defer(scanent, errstr); - - /* Remove the server from the list. XXX We should free the memory */ - num_servers--; - for (i = current_server; i < num_servers; i++) - clamd_address_vector[i] = clamd_address_vector[i+1]; + if (cd->retry <= 0) break; + while (cd->retry > 0) cd->retry = sleep(cd->retry); } + if (malware_daemon_ctx.sock >= 0) + break; + + (void) m_panic_defer(scanent, CUS callout_address, errstr); - if (num_servers == 0) - return m_errlog_defer(scanent, US"all servers failed"); + /* Remove the server from the list. XXX We should free the memory */ + num_servers--; + for (; i < num_servers; i++) + cv[i] = cv[i+1]; } - else + + if (num_servers == 0) + return m_panic_defer(scanent, NULL, US"all servers failed"); + } + else + for (;;) { - if ((sock = m_unixsocket(scanner_options, &errstr)) < 0) - return m_errlog_defer(scanent, errstr); + if ((malware_daemon_ctx.sock = ip_unixsocket(cv[0]->hostspec, &errstr)) >= 0) + { + hostname = cv[0]->hostspec; + break; + } + if (cv[0]->retry <= 0) + return m_panic_defer(scanent, CUS callout_address, errstr); + while (cv[0]->retry > 0) cv[0]->retry = sleep(cv[0]->retry); } - /* 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. */ + /* 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. */ - if (!use_scan_command) - { -#ifdef WITH_OLD_CLAMAV_STREAM - /* "STREAM\n" command, get back a "PORT \n" response, send data to - * that port on a second connection; then in the scan-method-neutral - * part, read the response back on the original connection. */ - - DEBUG(D_acl) debug_printf("Malware scan: issuing %s old-style remote scan (PORT)\n", - scanner_name); - - /* Pass the string to ClamAV (7 = "STREAM\n") */ - if (m_sock_send(sock, US"STREAM\n", 7, &errstr) < 0) - return m_errlog_defer(scanent, errstr); - - memset(av_buffer2, 0, sizeof(av_buffer2)); - bread = ip_recv(sock, av_buffer2, sizeof(av_buffer2), tmo-time(NULL)); - - if (bread < 0) - return m_errlog_defer_3(scanent, - string_sprintf("unable to read PORT from socket (%s)", - strerror(errno)), - sock); - - if (bread == sizeof(av_buffer2)) - return m_errlog_defer_3(scanent, "buffer too small", sock); - - if (!(*av_buffer2)) - return m_errlog_defer_3(scanent, "ClamAV returned null", sock); - - av_buffer2[bread] = '\0'; - if( sscanf(CS av_buffer2, "PORT %u\n", &port) != 1 ) - return m_errlog_defer_3(scanent, - string_sprintf("Expected port information from clamd, got '%s'", - av_buffer2), - sock); - - sockData = m_tcpsocket(connhost.address, port, NULL, &errstr); - if (sockData < 0) - return m_errlog_defer_3(scanent, errstr, sock); - -# define CLOSE_SOCKDATA (void)close(sockData) -#else /* WITH_OLD_CLAMAV_STREAM not defined */ - /* New protocol: "zINSTREAM\n" followed by a sequence of - chunks, a 4-byte number (network order), terminated by a zero-length - chunk. */ - - DEBUG(D_acl) debug_printf("Malware scan: issuing %s new-style remote scan (zINSTREAM)\n", - scanner_name); - - /* Pass the string to ClamAV (10 = "zINSTREAM\0") */ - if (send(sock, "zINSTREAM", 10, 0) < 0) - return m_errlog_defer_3(scanent, + if (!use_scan_command) + { + /* New protocol: "zINSTREAM\n" followed by a sequence of + chunks, a 4-byte number (network order), terminated by a zero-length + chunk. */ + + DEBUG(D_acl) debug_printf_indent( + "Malware scan: issuing %s new-style remote scan (zINSTREAM)\n", + scanner_name); + + /* 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) + return m_panic_defer_3(scanent, CUS hostname, string_sprintf("unable to send zINSTREAM to socket (%s)", strerror(errno)), - sock); - -# define CLOSE_SOCKDATA /**/ -#endif - - /* calc file size */ - if ((clam_fd = open(CS eml_filename, O_RDONLY)) < 0) - { - int err = errno; - CLOSE_SOCKDATA; - return m_errlog_defer_3(scanent, - string_sprintf("can't open spool file %s: %s", - eml_filename, strerror(err)), - sock); - } - if ((fsize = lseek(clam_fd, 0, SEEK_END)) < 0) - { - int err = errno; - CLOSE_SOCKDATA; (void)close(clam_fd); - return m_errlog_defer_3(scanent, - string_sprintf("can't seek spool file %s: %s", - eml_filename, strerror(err)), - sock); - } - fsize_uint = (unsigned int) fsize; - if ((off_t)fsize_uint != fsize) - { - CLOSE_SOCKDATA; (void)close(clam_fd); - return m_errlog_defer_3(scanent, - string_sprintf("seeking spool file %s, size overflow", - eml_filename), - sock); - } - lseek(clam_fd, 0, SEEK_SET); + malware_daemon_ctx.sock); - if (!(clamav_fbuf = (uschar *) malloc (fsize_uint))) - { - CLOSE_SOCKDATA; (void)close(clam_fd); - return m_errlog_defer_3(scanent, - string_sprintf("unable to allocate memory %u for file (%s)", - fsize_uint, eml_filename), - sock); - } - - if ((result = read(clam_fd, clamav_fbuf, fsize_uint)) < 0) - { - int err = errno; - free(clamav_fbuf); CLOSE_SOCKDATA; (void)close(clam_fd); - return m_errlog_defer_3(scanent, - string_sprintf("can't read spool file %s: %s", - eml_filename, strerror(err)), - sock); - } + /* calc file size */ + if ((clam_fd = exim_open2(CS eml_filename, O_RDONLY)) < 0) + { + int err = errno; + return m_panic_defer_3(scanent, NULL, + string_sprintf("can't open spool file %s: %s", + eml_filename, strerror(err)), + malware_daemon_ctx.sock); + } + if ((fsize = lseek(clam_fd, 0, SEEK_END)) < 0) + { + int err; +b_seek: err = errno; (void)close(clam_fd); - - /* send file body to socket */ -#ifdef WITH_OLD_CLAMAV_STREAM - if (send(sockData, clamav_fbuf, fsize_uint, 0) < 0) - { - free(clamav_fbuf); CLOSE_SOCKDATA; - return m_errlog_defer_3(scanent, - string_sprintf("unable to send file body to socket (%s:%u)", - hostname, port), - sock); - } -#else - send_size = htonl(fsize_uint); - send_final_zeroblock = 0; - if ((send(sock, &send_size, sizeof(send_size), 0) < 0) || - (send(sock, clamav_fbuf, fsize_uint, 0) < 0) || - (send(sock, &send_final_zeroblock, sizeof(send_final_zeroblock), 0) < 0)) - { - free(clamav_fbuf); - return m_errlog_defer_3(scanent, - string_sprintf("unable to send file body to socket (%s)", hostname), - sock); - } -#endif - - free(clamav_fbuf); - - CLOSE_SOCKDATA; -#undef CLOSE_SOCKDATA + return m_panic_defer_3(scanent, NULL, + string_sprintf("can't seek spool file %s: %s", + eml_filename, strerror(err)), + malware_daemon_ctx.sock); } - 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 */ - - /* ================================================================= */ - - /* Prior to the reworking post-Exim-4.72, this scanned a directory, - which dates to when ClamAV needed us to break apart the email into the - MIME parts (eg, with the now deprecated demime condition coming first). - Some time back, ClamAV gained the ability to deconstruct the emails, so - doing this would actually have resulted in the mail attachments being - scanned twice, in the broken out files and from the original .eml. - Since ClamAV now handles emails (and has for quite some time) we can - just use the email file itself. */ - /* Pass the string to ClamAV (7 = "SCAN \n" + \0) */ - file_name = string_sprintf("SCAN %s\n", eml_filename); - - DEBUG(D_acl) debug_printf("Malware scan: issuing %s local-path scan [%s]\n", - scanner_name, scanner_options); - - if (send(sock, file_name, Ustrlen(file_name), 0) < 0) - return m_errlog_defer_3(scanent, - string_sprintf("unable to write to socket (%s)", strerror(errno)), - sock); - - /* Do not shut down the socket for writing; a user report noted that - * clamd 0.70 does not react well to this. */ + fsize_uint = (unsigned int) fsize; + if ((off_t)fsize_uint != fsize) + { + (void)close(clam_fd); + return m_panic_defer_3(scanent, NULL, + string_sprintf("seeking spool file %s, size overflow", + eml_filename), + malware_daemon_ctx.sock); } - /* Commands have been sent, no matter which scan method or connection - * type we're using; now just read the result, independent of method. */ - - /* Read the result */ - memset(av_buffer, 0, sizeof(av_buffer)); - bread = ip_recv(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL)); - (void)close(sock); - sock = -1; - - if (bread <= 0) - return m_errlog_defer(scanent, - string_sprintf("unable to read from socket (%s)", strerror(errno))); - - if (bread == sizeof(av_buffer)) - return m_errlog_defer(scanent, US"buffer too small"); - /* We're now assured of a NULL at the end of av_buffer */ - - /* Check the result. ClamAV returns one of two result formats. - In the basic mode, the response is of the form: - infected: -> ": FOUND" - not-infected: -> ": OK" - error: -> ": ERROR - If the ExtendedDetectionInfo option has been turned on, then we get: - ": (:) FOUND" - for the infected case. Compare: - /tmp/eicar.com: Eicar-Test-Signature FOUND - /tmp/eicar.com: Eicar-Test-Signature(44d88612fea8a8f36de82e1278abb02f:68) FOUND - - In the streaming case, clamd uses the filename "stream" which you should - be able to verify with { ktrace clamdscan --stream /tmp/eicar.com }. (The - client app will replace "stream" with the original filename before returning - results to stdout, but the trace shows the data). - - We will assume that the pathname passed to clamd from Exim does not contain - a colon. We will have whined loudly above if the eml_filename does (and we're - passing a filename to clamd). */ - - if (!(*av_buffer)) - return m_errlog_defer(scanent, US"ClamAV returned null"); - - /* 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'; - - DEBUG(D_acl) debug_printf("Malware response: %s\n", av_buffer); - - while (isspace(*--p) && (p > av_buffer)) - *p = '\0'; - if (*p) ++p; - response_end = p; - - /* colon in returned output? */ - if((p = Ustrchr(av_buffer,':')) == NULL) - return m_errlog_defer(scanent, string_sprintf( - "ClamAV returned malformed result (missing colon): %s", - av_buffer)); - - /* strip filename */ - while (*p && isspace(*++p)) /**/; - vname = p; + if (lseek(clam_fd, 0, SEEK_SET) < 0) + goto b_seek; - /* It would be bad to encounter a virus with "FOUND" in part of the name, - but we should at least be resistant to it. */ - p = Ustrrchr(vname, ' '); - result_tag = p ? p+1 : vname; - - if (Ustrcmp(result_tag, "FOUND") == 0) + if (!(clamav_fbuf = store_malloc(fsize_uint))) { - /* p should still be the whitespace before the result_tag */ - while (isspace(*p)) --p; - *++p = '\0'; - /* Strip off the extended information too, which will be in parens - after the virus name, with no intervening whitespace. */ - if (*--p == ')') - { - /* "(hash:size)", so previous '(' will do; if not found, we have - a curious virus name, but not an error. */ - p = Ustrrchr(vname, '('); - if (p) - *p = '\0'; - } - malware_name = string_copy(vname); - DEBUG(D_acl) debug_printf("Malware found, name \"%s\"\n", malware_name); - + (void)close(clam_fd); + return m_panic_defer_3(scanent, NULL, + string_sprintf("unable to allocate memory %u for file (%s)", + fsize_uint, eml_filename), + malware_daemon_ctx.sock); } - else if (Ustrcmp(result_tag, "ERROR") == 0) - return m_errlog_defer(scanent, - string_sprintf("ClamAV returned: %s", av_buffer)); - else if (Ustrcmp(result_tag, "OK") == 0) + if ((result = read(clam_fd, clamav_fbuf, fsize_uint)) < 0) { - /* Everything should be OK */ - malware_name = NULL; - DEBUG(D_acl) debug_printf("Malware not found\n"); - + int err = errno; + store_free(clamav_fbuf); (void)close(clam_fd); + return m_panic_defer_3(scanent, NULL, + string_sprintf("can't read spool file %s: %s", + eml_filename, strerror(err)), + malware_daemon_ctx.sock); } - else - return m_errlog_defer(scanent, - string_sprintf("unparseable response from ClamAV: {%s}", av_buffer)); - - break; - } /* clamd */ - - case M_SOCK: /* "sock" scanner type ------------------------------------- */ - /* This code was derived by Martin Poole from the clamd code contributed - by David Saez and the cmdline code - */ - { - int bread; - uschar * commandline; - uschar av_buffer[1024]; - uschar * linebuffer; - uschar * sockline_scanner; - uschar sockline_scanner_default[] = "%s\n"; - const pcre *sockline_trig_re; - const pcre *sockline_name_re; - - /* find scanner command line */ - if ((sockline_scanner = string_nextinlist(&av_scanner_work, &sep, - NULL, 0))) - { /* check for no expansions apart from one %s */ - uschar * s = Ustrchr(sockline_scanner, '%'); - if (s++) - if ((*s != 's' && *s != '%') || Ustrchr(s+1, '%')) - return m_errlog_defer_3(scanent, - US"unsafe sock scanner call spec", sock); + (void)close(clam_fd); + + /* send file body to socket */ + send_size = htonl(fsize_uint); + send_final_zeroblock = 0; + if ((send(malware_daemon_ctx.sock, &send_size, sizeof(send_size), 0) < 0) || + (send(malware_daemon_ctx.sock, clamav_fbuf, fsize_uint, 0) < 0) || + (send(malware_daemon_ctx.sock, &send_final_zeroblock, sizeof(send_final_zeroblock), 0) < 0)) + { + store_free(clamav_fbuf); + return m_panic_defer_3(scanent, NULL, + string_sprintf("unable to send file body to socket (%s)", hostname), + malware_daemon_ctx.sock); + } + store_free(clamav_fbuf); } - else - sockline_scanner = sockline_scanner_default; - - /* find scanner output trigger */ - sockline_trig_re = m_pcre_nextinlist(&av_scanner_work, &sep, - "missing trigger specification", &errstr); - if (!sockline_trig_re) - return m_errlog_defer_3(scanent, errstr, sock); - - /* find virus name regex */ - sockline_name_re = m_pcre_nextinlist(&av_scanner_work, &sep, - "missing virus name regex specification", &errstr); - if (!sockline_name_re) - return m_errlog_defer_3(scanent, errstr, sock); + 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 */ + +/* ================================================================= */ + + /* Prior to the reworking post-Exim-4.72, this scanned a directory, + which dates to when ClamAV needed us to break apart the email into the + MIME parts (eg, with the now deprecated demime condition coming first). + Some time back, ClamAV gained the ability to deconstruct the emails, so + doing this would actually have resulted in the mail attachments being + scanned twice, in the broken out files and from the original .eml. + Since ClamAV now handles emails (and has for quite some time) we can + just use the email file itself. */ + /* Pass the string to ClamAV (7 = "SCAN \n" + \0), if not already sent */ + + DEBUG(D_acl) debug_printf_indent( + "Malware scan: issuing %s local-path scan [%s]\n", + scanner_name, scanner_options); - /* prepare scanner call - security depends on expansions check above */ - commandline = string_sprintf("%s/scan/%s/%s.eml", spool_directory, message_id, message_id); - commandline = string_sprintf( CS sockline_scanner, CS commandline); + if (cmd_str.len) + if (send(malware_daemon_ctx.sock, cmd_str.data, cmd_str.len, 0) < 0) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to write to socket (%s)", strerror(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. */ + } + /* Commands have been sent, no matter which scan method or connection + * type we're using; now just read the result, independent of method. */ + + /* Read the result */ + memset(av_buffer, 0, sizeof(av_buffer)); + bread = ip_recv(&malware_daemon_ctx, av_buffer, sizeof(av_buffer), tmo); + (void)close(malware_daemon_ctx.sock); + malware_daemon_ctx.sock = -1; + malware_daemon_ctx.tls_ctx = NULL; + + if (bread <= 0) + return m_panic_defer(scanent, CUS callout_address, + string_sprintf("unable to read from socket (%s)", + errno == 0 ? "EOF" : strerror(errno))); + + if (bread == sizeof(av_buffer)) + return m_panic_defer(scanent, CUS callout_address, + US"buffer too small"); + /* We're now assured of a NULL at the end of av_buffer */ + + /* Check the result. ClamAV returns one of two result formats. + In the basic mode, the response is of the form: + infected: -> ": FOUND" + not-infected: -> ": OK" + error: -> ": ERROR + If the ExtendedDetectionInfo option has been turned on, then we get: + ": (:) FOUND" + for the infected case. Compare: +/tmp/eicar.com: Eicar-Test-Signature FOUND +/tmp/eicar.com: Eicar-Test-Signature(44d88612fea8a8f36de82e1278abb02f:68) FOUND + + In the streaming case, clamd uses the filename "stream" which you should + be able to verify with { ktrace clamdscan --stream /tmp/eicar.com }. (The + client app will replace "stream" with the original filename before returning + results to stdout, but the trace shows the data). + + We will assume that the pathname passed to clamd from Exim does not contain + a colon. We will have whined loudly above if the eml_filename does (and we're + passing a filename to clamd). */ + + if (!(*av_buffer)) + return m_panic_defer(scanent, CUS callout_address, + US"ClamAV returned null"); + + /* 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'; + + DEBUG(D_acl) debug_printf_indent("Malware response: %s\n", av_buffer); + + while (isspace(*--p) && (p > av_buffer)) + *p = '\0'; + if (*p) ++p; + + /* colon in returned output? */ + if(!(p = Ustrchr(av_buffer,':'))) + return m_panic_defer(scanent, CUS callout_address, string_sprintf( + "ClamAV returned malformed result (missing colon): %s", + av_buffer)); + + /* strip filename */ + while (*p && isspace(*++p)) /**/; + vname = p; + + /* It would be bad to encounter a virus with "FOUND" in part of the name, + but we should at least be resistant to it. */ + p = Ustrrchr(vname, ' '); + result_tag = p ? p+1 : vname; + + if (Ustrcmp(result_tag, "FOUND") == 0) + { + /* p should still be the whitespace before the result_tag */ + while (isspace(*p)) --p; + *++p = '\0'; + /* Strip off the extended information too, which will be in parens + after the virus name, with no intervening whitespace. */ + if (*--p == ')') + { + /* "(hash:size)", so previous '(' will do; if not found, we have + a curious virus name, but not an error. */ + p = Ustrrchr(vname, '('); + if (p) + *p = '\0'; + } + malware_name = string_copy(vname); + DEBUG(D_acl) debug_printf_indent("Malware found, name \"%s\"\n", malware_name); - /* Pass the command string to the socket */ - if (m_sock_send(sock, commandline, Ustrlen(commandline), &errstr) < 0) - return m_errlog_defer(scanent, errstr); + } + else if (Ustrcmp(result_tag, "ERROR") == 0) + return m_panic_defer(scanent, CUS callout_address, + string_sprintf("ClamAV returned: %s", av_buffer)); - /* Read the result */ - bread = ip_recv(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL)); + else if (Ustrcmp(result_tag, "OK") == 0) + { + /* Everything should be OK */ + malware_name = NULL; + DEBUG(D_acl) debug_printf_indent("Malware not found\n"); - if (bread <= 0) - return m_errlog_defer_3(scanent, - string_sprintf("unable to read from socket (%s)", strerror(errno)), - sock); + } + else + return m_panic_defer(scanent, CUS callout_address, + string_sprintf("unparseable response from ClamAV: {%s}", av_buffer)); - if (bread == sizeof(av_buffer)) - return m_errlog_defer_3(scanent, US"buffer too small", sock); - av_buffer[bread] = '\0'; - linebuffer = string_copy(av_buffer); + break; + } /* clamd */ +#endif - /* try trigger match */ - if (regex_match_and_setup(sockline_trig_re, linebuffer, 0, -1)) { - if (!(malware_name = m_pcre_exec(sockline_name_re, av_buffer))) - malware_name = US "unknown"; +#ifndef DISABLE_MAL_SOCK + case M_SOCK: /* "sock" scanner type ------------------------------------- */ + /* This code was derived by Martin Poole from the clamd code contributed + by David Saez and the cmdline code + */ + { + int bread; + uschar * commandline; + uschar av_buffer[1024]; + uschar * linebuffer; + uschar * sockline_scanner; + uschar sockline_scanner_default[] = "%s\n"; + const pcre *sockline_trig_re; + const pcre *sockline_name_re; + + /* find scanner command line */ + if ( (sockline_scanner = string_nextinlist(&av_scanner_work, &sep, + NULL, 0)) + && *sockline_scanner + ) + { /* check for no expansions apart from one %s */ + uschar * s = Ustrchr(sockline_scanner, '%'); + if (s++) + if ((*s != 's' && *s != '%') || Ustrchr(s+1, '%')) + return m_panic_defer_3(scanent, NULL, + US"unsafe sock scanner call spec", malware_daemon_ctx.sock); + } + else + sockline_scanner = sockline_scanner_default; + DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "cmdline: ", + string_printing(sockline_scanner)); + + /* find scanner output trigger */ + sockline_trig_re = m_pcre_nextinlist(&av_scanner_work, &sep, + "missing trigger specification", &errstr); + if (!sockline_trig_re) + return m_panic_defer_3(scanent, NULL, errstr, malware_daemon_ctx.sock); + + /* find virus name regex */ + sockline_name_re = m_pcre_nextinlist(&av_scanner_work, &sep, + "missing virus name regex specification", &errstr); + if (!sockline_name_re) + return m_panic_defer_3(scanent, NULL, errstr, malware_daemon_ctx.sock); + + /* prepare scanner call - security depends on expansions check above */ + commandline = string_sprintf( CS sockline_scanner, CS eml_filename); + DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "expanded: ", + string_printing(commandline)); + + /* Pass the command string to the socket */ + if (m_sock_send(malware_daemon_ctx.sock, commandline, Ustrlen(commandline), &errstr) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); + + /* Read the result */ + bread = ip_recv(&malware_daemon_ctx, av_buffer, sizeof(av_buffer), tmo); + + if (bread <= 0) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to read from socket (%s)", strerror(errno)), + malware_daemon_ctx.sock); + + if (bread == sizeof(av_buffer)) + return m_panic_defer_3(scanent, CUS callout_address, + US"buffer too small", malware_daemon_ctx.sock); + av_buffer[bread] = '\0'; + linebuffer = string_copy(av_buffer); + DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "answer: ", + string_printing(linebuffer)); + + /* try trigger match */ + if (regex_match_and_setup(sockline_trig_re, linebuffer, 0, -1)) + { + if (!(malware_name = m_pcre_exec(sockline_name_re, av_buffer))) + malware_name = US "unknown"; + DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "name: ", + string_printing(malware_name)); } - else /* no virus found */ - malware_name = NULL; - break; + else /* no virus found */ + malware_name = NULL; + break; } +#endif +#ifndef DISABLE_MAL_MKS case M_MKSD: /* "mksd" scanner type ------------------------------------- */ { - char *mksd_options_end; - int mksd_maxproc = 1; /* default, if no option supplied */ - int retval; + char *mksd_options_end; + int mksd_maxproc = 1; /* default, if no option supplied */ + int retval; - if (scanner_options) - { - mksd_maxproc = (int)strtol(CS scanner_options, &mksd_options_end, 10); - if ( *scanner_options == '\0' - || *mksd_options_end != '\0' - || mksd_maxproc < 1 - || mksd_maxproc > 32 - ) - return m_errlog_defer(scanent, - string_sprintf("invalid option '%s'", scanner_options)); - } + if (scanner_options) + { + mksd_maxproc = (int)strtol(CS scanner_options, &mksd_options_end, 10); + if ( *scanner_options == '\0' + || *mksd_options_end != '\0' + || mksd_maxproc < 1 + || mksd_maxproc > 32 + ) + return m_panic_defer(scanent, CUS callout_address, + string_sprintf("invalid option '%s'", scanner_options)); + } - if((sock = m_unixsocket(US "/var/run/mksd/socket", &errstr)) < 0) - return m_errlog_defer(scanent, errstr); + if((malware_daemon_ctx.sock = ip_unixsocket(US "/var/run/mksd/socket", &errstr)) < 0) + return m_panic_defer(scanent, CUS callout_address, errstr); - malware_name = NULL; + malware_name = NULL; - DEBUG(D_acl) debug_printf("Malware scan: issuing %s scan\n", scanner_name); + DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan\n", scanner_name); - if ((retval = mksd_scan_packed(scanent, sock, eml_filename, tmo)) != OK) - { - close (sock); - return retval; - } - break; + if ((retval = mksd_scan_packed(scanent, malware_daemon_ctx.sock, eml_filename, tmo)) != OK) + { + close (malware_daemon_ctx.sock); + return retval; + } + break; } +#endif + +#ifndef DISABLE_MAL_AVAST case M_AVAST: /* "avast" scanner type ----------------------------------- */ { - int ovector[1*3]; uschar buf[1024]; uschar * scanrequest; - const pcre * avast_clean_re, * avast_virus_re; enum {AVA_HELO, AVA_OPT, AVA_RSP, AVA_DONE} avast_stage; + int nread; + uschar * error_message = NULL; + BOOL more_data = FALSE; + BOOL strict = TRUE; /* According to Martin Tuma @avast the protocol uses "escaped whitespace", that is, every embedded whitespace is backslash @@ -1666,33 +1980,75 @@ malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking) and the [ ] marker. [+] - not infected [L] - infected - [E] - some error occured - Such marker follows the first non-escaped TAB. */ - if ( !(avast_clean_re = - m_pcre_compile(US"(?!\\\\)\\t\\[\\+\\]", &errstr)) - || !(avast_virus_re = - m_pcre_compile(US"(?!\\\\)\\t\\[L\\]\\d\\.\\d\\t\\d\\s(.*)", - &errstr)) + [E] - some error occurred + Such marker follows the first non-escaped TAB. For more information + see avast-protocol(5) + + We observed two cases: + -> SCAN /file + <- /file [E]0.0 Error 13 Permission denied + <- 451 SCAN Engine error 13 permission denied + + -> SCAN /file + <- /file… [E]3.0 Error 41120 The file is a decompression bomb + <- /file… [+]2.0 + <- /file… [+]2.0 0 Eicar Test Virus!!! + <- 200 SCAN OK + + If the scanner returns 4xx, DEFER is a good decision, combined + with a panic log entry, to get the admin's attention. + + If the scanner returns 200, we reject it as malware, if found any, + or, in case of an error, we set the malware message to the error + string. + + Some of the >= 42000 errors are message related - usually some + broken archives etc, but some of them are e.g. license related. + Once the license expires the engine starts returning errors for + every scanning attempt. I¹ have the full list of the error codes + but it is not a public API and is subject to change. It is hard + for me to say what you should do in case of an engine error. You + can have a “Treat * unscanned file as infection” policy or “Treat + unscanned file as clean” policy. ¹) Jakub Bednar + + */ + + if ( ( !ava_re_clean + && !(ava_re_clean = m_pcre_compile(ava_re_clean_str, &errstr))) + || ( !ava_re_virus + && !(ava_re_virus = m_pcre_compile(ava_re_virus_str, &errstr))) + || ( !ava_re_error + && !(ava_re_error = m_pcre_compile(ava_re_error_str, &errstr))) ) - return malware_errlog_defer(errstr); + return malware_panic_defer(errstr); /* wait for result */ - for (avast_stage = AVA_HELO; recv_line(sock, buf, sizeof(buf), tmo) > 0; ) + for (avast_stage = AVA_HELO; + (nread = recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo)) > 0; + ) { int slen = Ustrlen(buf); - if (slen >= 1) + if (slen >= 1) { - DEBUG(D_acl) debug_printf("got from avast: %s\n", buf); + + /* Multi line responses are bracketed between 210 … and nnn … */ + if (Ustrncmp(buf, "210", 3) == 0) + { + more_data = 1; + continue; + } + else if (more_data && isdigit(buf[0])) more_data = 0; + switch (avast_stage) { case AVA_HELO: + if (more_data) continue; if (Ustrncmp(buf, "220", 3) != 0) goto endloop; /* require a 220 */ goto sendreq; case AVA_OPT: - if (Ustrncmp(buf, "210", 3) == 0) - break; /* ignore 210 responses */ + if (more_data) continue; if (Ustrncmp(buf, "200", 3) != 0) goto endloop; /* require a 200 */ @@ -1703,207 +2059,240 @@ malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking) if ((scanrequest = string_nextinlist(&av_scanner_work, &sep, NULL, 0))) { + if (Ustrcmp(scanrequest, "pass_unscanned") == 0) + { + DEBUG(D_acl) debug_printf_indent("pass unscanned files as clean\n"); + strict = FALSE; + goto sendreq; + } scanrequest = string_sprintf("%s\n", scanrequest); avast_stage = AVA_OPT; /* just sent option */ + DEBUG(D_acl) debug_printf_indent("send to avast OPTION: %s", scanrequest); } else { - scanrequest = string_sprintf("SCAN %s/scan/%s\n", - spool_directory, message_id); + scanrequest = string_sprintf("SCAN %s\n", eml_dir); avast_stage = AVA_RSP; /* just sent command */ + DEBUG(D_acl) debug_printf_indent("send to avast REQUEST: SCAN %s\n", eml_dir); } /* send config-cmd or scan-request to socket */ len = Ustrlen(scanrequest); - if (send(sock, scanrequest, len, 0) < 0) + if (send(malware_daemon_ctx.sock, scanrequest, len, 0) == -1) { scanrequest[len-1] = '\0'; - return m_errlog_defer_3(scanent, string_sprintf( + return m_panic_defer_3(scanent, CUS callout_address, string_sprintf( "unable to send request '%s' to socket (%s): %s", - scanrequest, scanner_options, strerror(errno)), sock); + scanrequest, scanner_options, strerror(errno)), malware_daemon_ctx.sock); } break; } case AVA_RSP: - if (Ustrncmp(buf, "210", 3) == 0) - break; /* ignore the "210 SCAN DATA" message */ - if (pcre_exec(avast_clean_re, NULL, CS buf, slen, - 0, 0, ovector, nelements(ovector)) > 0) - break; + if (isdigit(buf[0])) /* We're done */ + goto endloop; - if ((malware_name = m_pcre_exec(avast_virus_re, buf))) - { /* remove backslash in front of [whitespace|backslash] */ - uschar * p, * p0; - for (p = malware_name; *p; ++p) - if (*p == '\\' && (isspace(p[1]) || p[1] == '\\')) - for (p0 = p; *p0; ++p0) *p0 = p0[1]; - - avast_stage = AVA_DONE; - goto endloop; - } + if (malware_name) /* Nothing else matters, just read on */ + break; - if (Ustrncmp(buf, "200 SCAN OK", 11) == 0) - { /* we're done finally */ - if (send(sock, "QUIT\n", 5, 0) < 0) /* courtesy */ - return m_errlog_defer_3(scanent, string_sprintf( - "unable to send quit request to socket (%s): %s", - scanner_options, strerror(errno)), - sock); - malware_name = NULL; - avast_stage = AVA_DONE; - goto endloop; - } + if (pcre_exec(ava_re_clean, NULL, CS buf, slen, 0, 0, NULL, 0) == 0) + break; + + if ((malware_name = m_pcre_exec(ava_re_virus, buf))) + { + unescape(malware_name); + DEBUG(D_acl) + debug_printf_indent("unescaped malware name: '%s'\n", malware_name); + break; + } + + if (strict) /* treat scanner errors as malware */ + { + if ((malware_name = m_pcre_exec(ava_re_error, buf))) + { + unescape(malware_name); + DEBUG(D_acl) + debug_printf_indent("unescaped error message: '%s'\n", malware_name); + break; + } + } + else if (pcre_exec(ava_re_error, NULL, CS buf, slen, 0, 0, NULL, 0) == 0) + { + log_write(0, LOG_MAIN, "internal scanner error (ignored): %s", buf); + break; + } + + /* here also for any unexpected response from the scanner */ + DEBUG(D_acl) debug_printf("avast response not handled: '%s'\n", buf); - /* here for any unexpected response from the scanner */ goto endloop; + + default: log_write(0, LOG_PANIC, "%s:%d:%s: should not happen", + __FILE__, __LINE__, __FUNCTION__); } + } } - } + endloop: - switch(avast_stage) - { - case AVA_HELO: - case AVA_OPT: - case AVA_RSP: return m_errlog_defer_3(scanent, string_sprintf( - "invalid response from scanner: %s\n", buf), sock); - default: break; - } + if (nread == -1) error_message = US"EOF from scanner"; + else if (nread < 0) error_message = US"timeout from scanner"; + else if (nread == 0) error_message = US"got nothing from scanner"; + else if (buf[0] != '2') error_message = buf; + + DEBUG(D_acl) debug_printf_indent("sent to avast QUIT\n"); + if (send(malware_daemon_ctx.sock, "QUIT\n", 5, 0) == -1) + return m_panic_defer_3(scanent, CUS callout_address, + string_sprintf("unable to send quit request to socket (%s): %s", + scanner_options, strerror(errno)), malware_daemon_ctx.sock); + + if (error_message) + return m_panic_defer_3(scanent, CUS callout_address, error_message, malware_daemon_ctx.sock); + } - } /* scanner type switch */ +#endif + } /* scanner type switch */ - if (sock >= 0) - (void) close (sock); - malware_ok = TRUE; /* set "been here, done that" marker */ + if (malware_daemon_ctx.sock >= 0) + (void) close (malware_daemon_ctx.sock); + malware_ok = TRUE; /* set "been here, done that" marker */ } - /* match virus name against pattern (caseless ------->----------v) */ - if (malware_name && regex_match_and_setup(re, malware_name, 0, -1)) - { - DEBUG(D_acl) debug_printf("Matched regex to malware [%s] [%s]\n", malware_regex, malware_name); - return OK; - } - else - return FAIL; +/* match virus name against pattern (caseless ------->----------v) */ +if (malware_name && regex_match_and_setup(re, malware_name, 0, -1)) + { + DEBUG(D_acl) debug_printf_indent( + "Matched regex to malware [%s] [%s]\n", malware_re, malware_name); + return OK; + } +else + return FAIL; } -/* ============= private routines for the "mksd" scanner type ============== */ +/************************************************* +* Scan an email for malware * +*************************************************/ + +/* This is the normal interface for scanning an email, which doesn't need a +filename; it's a wrapper around the malware_file function. -#include +Arguments: + malware_re match condition for "malware=" + timeout if nonzero, timeout in seconds -static inline int -mksd_writev (int sock, struct iovec *iov, int iovcnt) +Returns: Exim message processing code (OK, FAIL, DEFER, ...) + where true means malware was found (condition applies) +*/ +int +malware(const uschar * malware_re, int timeout) { - int i; - - for (;;) { - do - i = writev (sock, iov, iovcnt); - while ((i < 0) && (errno == EINTR)); - if (i <= 0) { - (void) malware_errlog_defer(US"unable to write to mksd UNIX socket (/var/run/mksd/socket)"); - return -1; - } +int ret = malware_internal(malware_re, NULL, timeout); - for (;;) - if (i >= iov->iov_len) { - if (--iovcnt == 0) - return 0; - i -= iov->iov_len; - iov++; - } else { - iov->iov_len -= i; - iov->iov_base = CS iov->iov_base + i; - break; - } - } +if (ret == DEFER) av_failed = TRUE; +return ret; } -static inline int -mksd_read_lines (int sock, uschar *av_buffer, int av_buffer_size, int tmo) -{ - int offset = 0; - int i; - do - { - i = ip_recv(sock, av_buffer+offset, av_buffer_size-offset, tmo-time(NULL)); - if (i <= 0) - { - (void) malware_errlog_defer(US"unable to read from mksd UNIX socket (/var/run/mksd/socket)"); - return -1; - } +/************************************************* +* Scan a file for malware * +*************************************************/ - offset += i; - /* offset == av_buffer_size -> buffer full */ - if (offset == av_buffer_size) - { - (void) malware_errlog_defer(US"malformed reply received from mksd"); - return -1; - } - } while (av_buffer[offset-1] != '\n'); +/* This is a test wrapper for scanning an email, which is not used in +normal processing. Scan any file, using the Exim scanning interface. +This function tampers with various global variables so is unsafe to use +in any other context. - av_buffer[offset] = '\0'; - return offset; -} +Arguments: + eml_filename a file holding the message to be scanned -static inline int -mksd_parse_line(struct scan * scanent, char *line) +Returns: Exim message processing code (OK, FAIL, DEFER, ...) + where true means malware was found (condition applies) +*/ +int +malware_in_file(uschar *eml_filename) { - char *p; - - switch (*line) { - case 'O': /* OK */ - return OK; - - case 'E': - case 'A': /* ERR */ - if ((p = strchr (line, '\n')) != NULL) - *p = '\0'; - return m_errlog_defer(scanent, - string_sprintf("scanner failed: %s", line)); - - default: /* VIR */ - if ((p = strchr (line, '\n')) != NULL) { - *p = '\0'; - if (((p-line) > 5) && (line[3] == ' ')) - if (((p = strchr (line+4, ' ')) != NULL) && ((p-line) > 4)) { - *p = '\0'; - malware_name = string_copy(US line+4); - return OK; - } - } - return m_errlog_defer(scanent, - string_sprintf("malformed reply received: %s", line)); - } +uschar message_id_buf[64]; +int ret; + +/* spool_mbox() assumes various parameters exist, when creating +the relevant directory and the email within */ + +(void) string_format(message_id_buf, sizeof(message_id_buf), + "dummy-%d", vaguely_random_number(INT_MAX)); +message_id = message_id_buf; +sender_address = US"malware-sender@example.net"; +return_path = US""; +recipients_list = NULL; +receive_add_recipient(US"malware-victim@example.net", -1); +f.enable_dollar_recipients = TRUE; + +ret = malware_internal(US"*", eml_filename, 0); + +Ustrncpy(spooled_message_id, message_id, sizeof(spooled_message_id)); +spool_mbox_ok = 1; + +/* don't set no_mbox_unspool; at present, there's no way for it to become +set, but if that changes, then it should apply to these tests too */ + +unspool_mbox(); + +/* silence static analysis tools */ +message_id = NULL; + +return ret; } -static int -mksd_scan_packed(struct scan * scanent, int sock, uschar *scan_filename, int tmo) -{ - struct iovec iov[3]; - const char *cmd = "MSQ\n"; - uschar av_buffer[1024]; - iov[0].iov_base = (void *) cmd; - iov[0].iov_len = 3; - iov[1].iov_base = CS scan_filename; - iov[1].iov_len = Ustrlen(scan_filename); - iov[2].iov_base = (void *) (cmd + 3); - iov[2].iov_len = 1; +void +malware_init(void) +{ +if (!malware_default_re) + malware_default_re = regex_must_compile(malware_regex_default, FALSE, TRUE); - if (mksd_writev (sock, iov, 3) < 0) - return DEFER; +#ifndef DISABLE_MAL_DRWEB +if (!drweb_re) + drweb_re = regex_must_compile(drweb_re_str, FALSE, TRUE); +#endif +#ifndef DISABLE_MAL_FSECURE +if (!fsec_re) + fsec_re = regex_must_compile(fsec_re_str, FALSE, TRUE); +#endif +#ifndef DISABLE_MAL_KAV +if (!kav_re_sus) + kav_re_sus = regex_must_compile(kav_re_sus_str, FALSE, TRUE); +if (!kav_re_inf) + kav_re_inf = regex_must_compile(kav_re_inf_str, FALSE, TRUE); +#endif +#ifndef DISABLE_MAL_AVAST +if (!ava_re_clean) + ava_re_clean = regex_must_compile(ava_re_clean_str, FALSE, TRUE); +if (!ava_re_virus) + ava_re_virus = regex_must_compile(ava_re_virus_str, FALSE, TRUE); +if (!ava_re_error) + ava_re_error = regex_must_compile(ava_re_error_str, FALSE, TRUE); +#endif +#ifndef DISABLE_MAL_FFROT6D +if (!fprot6d_re_error) + fprot6d_re_error = regex_must_compile(fprot6d_re_error_str, FALSE, TRUE); +if (!fprot6d_re_virus) + fprot6d_re_virus = regex_must_compile(fprot6d_re_virus_str, FALSE, TRUE); +#endif +} - if (mksd_read_lines (sock, av_buffer, sizeof (av_buffer), tmo) < 0) - return DEFER; - return mksd_parse_line (scanent, CS av_buffer); +gstring * +malware_show_supported(gstring * g) +{ +g = string_cat(g, US"Malware:"); +for (struct scan * sc = m_scans; sc->scancode != (scanner_t)-1; sc++) + g = string_fmt_append(g, " %s", sc->name); +return string_cat(g, US"\n"); } + +# endif /*!MACRO_PREDEF*/ #endif /*WITH_CONTENT_SCAN*/ /* * vi: aw ai sw=2