X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/eb445b049c9b78cbe187b9cb3c318d65862d4851..4a852e8c97fa4de42c443107121c7717e1f0c9b2:/src/src/malware.c diff --git a/src/src/malware.c b/src/src/malware.c index 730a2be22..b34ca5794 100644 --- a/src/src/malware.c +++ b/src/src/malware.c @@ -4,7 +4,7 @@ /* Copyright (c) Tom Kistner 2003 - 2015 * License: GPL - * Copyright (c) The Exim Maintainers 2015 - 2018 + * Copyright (c) The Exim Maintainers 2015 - 2020 */ /* Code for calling virus (malware) scanners. Called from acl.c. */ @@ -12,8 +12,45 @@ #include "exim.h" #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, M_FPROT6D} 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 { @@ -70,16 +107,16 @@ static struct scan void features_malware(void) { -const struct scan * sc; const uschar * s; uschar * t; -uschar buf[64]; +uschar buf[EXIM_DRIVERNAME_MAX]; spf(buf, sizeof(buf), US"_HAVE_MALWARE_"); -for (sc = m_scans; sc->scancode != -1; sc++) +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); + for (s = sc->name, t = buf+14; *s; s++) if (*s != '-') + *t++ = toupper(*s); *t = '\0'; builtin_macro_create(buf); } @@ -92,8 +129,7 @@ for (sc = m_scans; sc->scancode != -1; sc++) #define MALWARE_TIMEOUT 120 /* default timeout, seconds */ static const uschar * malware_regex_default = US ".+"; -static const pcre * malware_default_re = NULL; - +static const pcre2_code * malware_default_re = NULL; #ifndef DISABLE_MAL_CLAM @@ -120,46 +156,47 @@ typedef struct clamd_address { # define DERR_BAD_CALL (1<<15) /* wrong command */ static const uschar * drweb_re_str = US "infected\\swith\\s*(.+?)$"; -static const pcre * drweb_re = NULL; +static const pcre2_code * drweb_re = NULL; #endif #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; +static const pcre2_code * fsec_re = NULL; #endif #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; +static const pcre2_code * kav_re_sus = NULL; +static const pcre2_code * 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; +static const pcre2_code * ava_re_clean = NULL; +static const pcre2_code * ava_re_virus = NULL; +static const pcre2_code * 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; +static const pcre2_code * fprot6d_re_error = NULL; +static const pcre2_code * 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() @@ -168,6 +205,7 @@ test_byte_order() char *byte = CS &word; return(byte[0] ? LITTLE_MY_ENDIAN : BIG_MY_ENDIAN); } +#endif BOOL malware_ok = FALSE; @@ -180,13 +218,17 @@ extern uschar spooled_message_id[MESSAGE_ID_LENGTH+1]; /* Some (currently avast only) use backslash escaped whitespace, this function undoes these escapes */ + +#ifndef DISABLE_MAL_AVAST static inline void -unescape(char *p) { - uschar *p0; - for (; *p; ++p) - if (*p == '\\' && (isspace(p[1]) || p[1] == '\\')) - for (p0 = p; *p0; ++p0) *p0 = p0[1]; +unescape(uschar *p) +{ +uschar *p0; +for (; *p; ++p) + if (*p == '\\' && (isspace(p[1]) || p[1] == '\\')) + for (p0 = p; *p0; ++p0) *p0 = p0[1]; } +#endif /* --- malware_*_defer --- */ static inline int @@ -209,28 +251,15 @@ m_panic_defer(struct scan * scanent, const uschar * hostport, return malware_panic_defer(string_sprintf("%s %s : %s", scanent->name, hostport ? hostport : CUS"", str)); } -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) { +DEBUG(D_acl) debug_print_socket(fd_to_close); (void) close(fd_to_close); return m_panic_defer(scanent, hostport, str); } -static inline int -m_log_defer_3(struct scan * scanent, const uschar * hostport, - const uschar * str, int fd_to_close) -{ -(void) close(fd_to_close); -return m_log_defer(scanent, hostport, str); -} /*************************************************/ @@ -242,8 +271,19 @@ static inline int m_tcpsocket(const uschar * hostname, unsigned int port, host_item * host, uschar ** errstr, const blob * fastopen_blob) { -return ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5, +int fd = ip_connectedsocket(SOCK_STREAM, hostname, port, port, 5, host, errstr, fastopen_blob); +#ifdef EXIM_TFO_FREEBSD +/* Under some fault conditions, FreeBSD 12.2 seen to send a (non-TFO) SYN +and, getting no response, wait for a long time. Impose a 5s max. */ +if (fd >= 0) + { + struct timeval tv = {.tv_sec = 5}; + fd_set fds; + FD_ZERO(&fds); FD_SET(fd, &fds); (void) select(fd+1, NULL, &fds, NULL, &tv); + } +#endif +return fd; } #endif @@ -261,38 +301,43 @@ if (send(sock, buf, cnt, 0) < 0) return sock; } -static const pcre * +static const pcre2_code * 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); +int err; +PCRE2_SIZE roffset; +const pcre2_code * cre; + +if (!(cre = pcre2_compile((PCRE2_SPTR)re, PCRE2_ZERO_TERMINATED, + PCRE_COPT, &err, &roffset, pcre_cmp_ctx))) + { + uschar errbuf[128]; + pcre2_get_error_message(err, errbuf, sizeof(errbuf)); + *errstr= string_sprintf("regular expression error in '%s': %s at offset %ld", + re, errbuf, (long)roffset); + } return cre; } uschar * -m_pcre_exec(const pcre * cre, uschar * text) +m_pcre_exec(const pcre2_code * cre, uschar * text) { -int ovector[10*3]; -int i = pcre_exec(cre, NULL, CS text, Ustrlen(text), 0, 0, - ovector, nelem(ovector)); -uschar * substr = NULL; +pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx); +int i = pcre2_match(cre, text, PCRE2_ZERO_TERMINATED, 0, 0, md, pcre_mtc_ctx); +PCRE2_UCHAR * substr = NULL; +PCRE2_SIZE slen; + if (i >= 2) /* Got it */ - pcre_get_substring(CS text, ovector, i, 1, (const char **) &substr); -return substr; + pcre2_substring_get_bynumber(md, 1, &substr, &slen); +return US substr; } -static const pcre * +static const pcre2_code * m_pcre_nextinlist(const uschar ** list, int * sep, char * listerr, uschar ** errstr) { const uschar * list_ele; -const pcre * cre = NULL; +const pcre2_code * cre = NULL; if (!(list_ele = string_nextinlist(list, sep, NULL, 0))) *errstr = US listerr; @@ -305,6 +350,7 @@ else return cre; } + /* Simple though inefficient wrapper for reading a line. Drop CRs and the trailing newline. Can return early on buffer full. Null-terminate. @@ -315,13 +361,13 @@ return cre; -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))) +if (!fd_ready(fd, tmo)) return -2; /*XXX tmo handling assumes we always get a whole line */ @@ -336,8 +382,12 @@ while ((rcv = read(fd, p, 1)) > 0) } if (!ok) { - DEBUG(D_acl) debug_printf_indent("Malware scan: read %s (%s)\n", + 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'; @@ -347,13 +397,15 @@ return p - buffer; } /* return TRUE iff size as requested */ +#ifndef DISABLE_MAL_DRWEB 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; } +#endif @@ -396,14 +448,15 @@ for (;;) } static inline int -mksd_read_lines (int sock, uschar *av_buffer, int av_buffer_size, int tmo) +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(sock, av_buffer+offset, av_buffer_size-offset, tmo-time(NULL)); + 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)"); @@ -462,7 +515,7 @@ switch (*line) static int mksd_scan_packed(struct scan * scanent, int sock, const uschar * scan_filename, - int tmo) + time_t tmo) { struct iovec iov[3]; const char *cmd = "MSQ\n"; @@ -534,11 +587,11 @@ const uschar *av_scanner_work = av_scanner; uschar *scanner_name; unsigned long mbox_size; FILE *mbox_file; -const pcre *re; +const pcre2_code *re; uschar * errstr; struct scan * scanent; const uschar * scanner_options; -int sock = -1; +client_conn_ctx malware_daemon_ctx = {.sock = -1}; time_t tmo; uschar * eml_filename, * eml_dir; @@ -619,12 +672,16 @@ if (!malware_ok) DEBUG(D_acl) debug_printf_indent("%15s%10s%s\n", "", "socket: ", scanner_options); switch(scanent->conn) { - case MC_TCP: sock = ip_tcpsocket(scanner_options, &errstr, 5); break; - case MC_UNIX: sock = ip_unixsocket(scanner_options, &errstr); break; - case MC_STRM: sock = ip_streamsocket(scanner_options, &errstr, 5); break; - default: /* compiler quietening */ break; + 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; } - if (sock < 0) + if (malware_daemon_ctx.sock < 0) return m_panic_defer(scanent, CUS callout_address, errstr); break; } @@ -655,10 +712,10 @@ if (!malware_ok) scanner_name, scanrequest); /* send scan request */ - if (m_sock_send(sock, scanrequest, Ustrlen(scanrequest)+1, &errstr) < 0) + 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) + while ((len = recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo)) >= 0) if (len > 0) { if (Ustrstr(buf, US" 0 */ if (drweb_vnum) { - int i; gstring * g = NULL; /* setup default virus name */ @@ -870,40 +927,40 @@ badseek: err = errno; drweb_re = m_pcre_compile(drweb_re_str, &errstr); /* read and concatenate virus names into one string */ - for (i = 0; i < drweb_vnum; i++) + for (int i = 0; i < drweb_vnum; i++) { - int ovector[10*3]; + pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx); /* read the size of report */ - if (!recv_len(sock, &drweb_slen, sizeof(drweb_slen), tmo)) + 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", sock); + US"cannot read report size", malware_daemon_ctx.sock); drweb_slen = ntohl(drweb_slen); - tmpbuf = store_get(drweb_slen); + + /* assume tainted, since it is external input */ + tmpbuf = store_get(drweb_slen, TRUE); /* read report body */ - if (!recv_len(sock, tmpbuf, drweb_slen, tmo)) + 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", sock); + 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)); + result = pcre2_match(drweb_re, (PCRE2_SPTR)tmpbuf, PCRE2_ZERO_TERMINATED, + 0, 0, md, pcre_mtc_ctx); if (result >= 2) { - const char * pre_malware_nb; - - pcre_get_substring(CS tmpbuf, ovector, result, 1, &pre_malware_nb); + PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md); if (i==0) /* the first name we just copy to malware_name */ - g = string_cat(NULL, US pre_malware_nb); + g = string_catn(NULL, US ovec[2], ovec[3] - ovec[2]); - /*XXX could be string_append_listele? */ else /* concatenate each new virus name to previous */ - g = string_append(g, 2, "/", pre_malware_nb); - - pcre_free_substring(pre_malware_nb); + { + g = string_catn(g, US"/", 1); + g = string_catn(g, US ovec[2], ovec[3] - ovec[2]); + } } } malware_name = string_from_gstring(g); @@ -923,7 +980,7 @@ badseek: err = errno; if (drweb_s) return m_panic_defer_3(scanent, CUS callout_address, string_sprintf("drweb daemon retcode 0x%x (%s)", drweb_rc, drweb_s), - sock); + malware_daemon_ctx.sock); /* no virus found */ malware_name = NULL; @@ -940,13 +997,13 @@ badseek: err = errno; /* read aveserver's greeting and see if it is ready (2xx greeting) */ buf[0] = 0; - recv_line(sock, buf, sizeof(buf), tmo); + 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") ), - sock); + malware_daemon_ctx.sock); /* prepare our command */ (void)string_format(buf, sizeof(buf), "SCAN bPQRSTUW %s\r\n", @@ -955,13 +1012,13 @@ badseek: err = errno; /* and send it */ DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s %s\n", scanner_name, buf); - if (m_sock_send(sock, buf, Ustrlen(buf), &errstr) < 0) + 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(sock, buf, sizeof(buf), tmo) > 0) + while (recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo) > 0) { if (buf[0] == '2') break; @@ -980,22 +1037,22 @@ badseek: err = errno; } } - if (m_sock_send(sock, US"quit\r\n", 6, &errstr) < 0) + 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(sock, buf, sizeof(buf), tmo); + 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") ), - sock); + malware_daemon_ctx.sock); if (result == DEFER) { - (void)close(sock); + (void)close(malware_daemon_ctx.sock); return DEFER; } break; @@ -1005,7 +1062,7 @@ badseek: err = errno; #ifndef DISABLE_MAL_FSECURE case M_FSEC: /* "fsecure" scanner type ---------------------------------- */ { - int i, j, bread = 0; + int i, bread = 0; uschar * file_name; uschar av_buffer[1024]; static uschar *cmdopt[] = { US"CONFIGURE\tARCHIVE\t1\n", @@ -1022,16 +1079,16 @@ badseek: err = errno; for (i = 0; i != nelem(cmdopt); i++) { - if (m_sock_send(sock, cmdopt[i], Ustrlen(cmdopt[i]), &errstr) < 0) + 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(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL)); + 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)), - sock); - for (j = 0; j < bread; j++) + malware_daemon_ctx.sock); + for (int j = 0; j < bread; j++) if (av_buffer[j] == '\r' || av_buffer[j] == '\n') av_buffer[j] ='@'; } @@ -1039,7 +1096,7 @@ badseek: err = errno; /* 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) + 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 */ @@ -1057,10 +1114,10 @@ badseek: err = errno; { errno = ETIMEDOUT; i = av_buffer+sizeof(av_buffer)-p; - if ((bread= ip_recv(sock, p, i-1, tmo-time(NULL))) < 0) + 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)), - sock); + malware_daemon_ctx.sock); for (p[bread] = '\0'; (q = Ustrchr(p, '\n')); p = q+1) { @@ -1096,7 +1153,7 @@ badseek: err = errno; int kav_rc; unsigned long kav_reportlen; int bread; - const pcre *kav_re; + const pcre2_code *kav_re; uschar *p; /* get current date and time, build scan request */ @@ -1117,13 +1174,13 @@ badseek: err = errno; scanner_name, scanner_options); /* send scan request */ - if (m_sock_send(sock, scanrequest, Ustrlen(scanrequest)+1, &errstr) < 0) + if (m_sock_send(malware_daemon_ctx.sock, scanrequest, Ustrlen(scanrequest)+1, &errstr) < 0) return m_panic_defer(scanent, CUS callout_address, errstr); /* wait for result */ - if (!recv_len(sock, tmpbuf, 2, tmo)) + 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.", sock); + US"unable to read 2 bytes from socket.", malware_daemon_ctx.sock); /* get errorcode from one nibble */ kav_rc = tmpbuf[ test_byte_order()==LITTLE_MY_ENDIAN ? 0 : 1 ] & 0x0F; @@ -1132,13 +1189,13 @@ badseek: err = errno; 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.", - sock); + malware_daemon_ctx.sock); case 1: return m_panic_defer_3(scanent, CUS callout_address, - US"reported 'scanning not completed' (code 1).", sock); + 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).", sock); + US"reported 'kavdaemon damaged' (code 7).", malware_daemon_ctx.sock); } /* code 8 is not handled, since it is ambiguous. It appears mostly on @@ -1158,9 +1215,9 @@ badseek: err = errno; if (report_flag == 1) { /* read report size */ - if (!recv_len(sock, &kav_reportlen, 4, tmo)) + 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", sock); + US"cannot read report size", malware_daemon_ctx.sock); /* it's possible that avp returns av_buffer[1] == 1 but the reportsize is 0 (!?) */ @@ -1183,7 +1240,7 @@ badseek: err = errno; /* coverity[tainted_data] */ while (kav_reportlen > 0) { - if ((bread = recv_line(sock, tmpbuf, sizeof(tmpbuf), tmo)) < 0) + if ((bread = recv_line(malware_daemon_ctx.sock, tmpbuf, sizeof(tmpbuf), tmo)) < 0) break; kav_reportlen -= bread+1; @@ -1205,8 +1262,8 @@ badseek: err = errno; case M_CMDL: /* "cmdline" scanner type ---------------------------------- */ { const uschar *cmdline_scanner = scanner_options; - const pcre *cmdline_trigger_re; - const pcre *cmdline_regex_re; + const pcre2_code *cmdline_trigger_re; + const pcre2_code *cmdline_regex_re; uschar * file_name; uschar * commandline; void (*eximsigchld)(int); @@ -1322,13 +1379,10 @@ badseek: err = errno; 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))) + 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); } else /* no virus found */ @@ -1353,19 +1407,19 @@ badseek: err = errno; DEBUG(D_acl) debug_printf_indent("Malware scan: issuing %s scan [%s]\n", scanner_name, scanner_options); - if ( write(sock, file_name, Ustrlen(file_name)) < 0 - || write(sock, "\n", 1) != 1 + 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), - sock); + malware_daemon_ctx.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) + 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), - sock); + malware_daemon_ctx.sock); /* infected ? */ if (av_buffer[0] == '1') { @@ -1376,7 +1430,7 @@ badseek: err = errno; } else if (!strncmp(CS av_buffer, "-1", 2)) return m_panic_defer_3(scanent, CUS callout_address, - US"scanner reported error", sock); + US"scanner reported error", malware_daemon_ctx.sock); else /* all ok, no virus */ malware_name = NULL; @@ -1403,9 +1457,7 @@ badseek: err = errno; 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]; @@ -1425,7 +1477,7 @@ badseek: err = errno; /* 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)); + cd = (clamd_address *) store_get(sizeof(clamd_address), FALSE); /* extract socket-path part */ sublist = scanner_options; @@ -1459,7 +1511,7 @@ badseek: err = errno; continue; } - cd = (clamd_address *) store_get(sizeof(clamd_address)); + cd = (clamd_address *) store_get(sizeof(clamd_address), FALSE); /* extract host and port part */ sublist = scanner_options; @@ -1511,17 +1563,14 @@ badseek: err = errno; 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); - } + cmd_str.data = string_sprintf("SCAN %s\n%n", eml_filename, &cmd_str.len); /* We have some network servers specified */ if (num_servers) { /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd - * only supports AF_INET, but we should probably be looking to the - * future and rewriting this to be protocol-independent anyway. */ + only supports AF_INET, but we should probably be looking to the + future and rewriting this to be protocol-independent anyway. */ while (num_servers > 0) { @@ -1532,21 +1581,23 @@ badseek: err = errno; cd->hostspec, cd->tcp_port); /* Lookup the host. This is to ensure that we connect to the same IP - * on both connections (as one host could resolve to multiple ips) */ + on both connections (as one host could resolve to multiple ips) */ for (;;) { - if ((sock = m_tcpsocket(cd->hostspec, cd->tcp_port, - &connhost, &errstr, &cmd_str)) >= 0) + /*XXX we trust that the cmd_str is idempotent */ + if ((malware_daemon_ctx.sock = m_tcpsocket(cd->hostspec, cd->tcp_port, + &connhost, &errstr, + use_scan_command ? &cmd_str : NULL)) >= 0) { /* Connection successfully established with a server */ hostname = cd->hostspec; - cmd_str.len = 0; + if (use_scan_command) cmd_str.len = 0; break; } if (cd->retry <= 0) break; while (cd->retry > 0) cd->retry = sleep(cd->retry); } - if (sock >= 0) + if (malware_daemon_ctx.sock >= 0) break; (void) m_panic_defer(scanent, CUS callout_address, errstr); @@ -1563,7 +1614,7 @@ badseek: err = errno; else for (;;) { - if ((sock = ip_unixsocket(cv[0]->hostspec, &errstr)) >= 0) + if ((malware_daemon_ctx.sock = ip_unixsocket(cv[0]->hostspec, &errstr)) >= 0) { hostname = cv[0]->hostspec; break; @@ -1574,98 +1625,117 @@ badseek: err = errno; } /* have socket in variable "sock"; command to use is semi-independent of - * the socket protocol. We use SCAN if is local (either Unix/local - * domain socket, or explicitly told local) else we stream the data. - * How we stream the data depends upon how we were built. */ + the socket protocol. We use SCAN if is local (either Unix/local + domain socket, or explicitly told local) else we stream the data. + How we stream the data depends upon how we were built. */ if (!use_scan_command) { + struct stat st; +#if defined(EXIM_TCP_CORK) && !defined(OS_SENDFILE) + BOOL corked = TRUE; +#endif /* New protocol: "zINSTREAM\n" followed by a sequence of chunks, a 4-byte number (network order), terminated by a zero-length - chunk. */ + chunk. We only send one chunk. */ DEBUG(D_acl) debug_printf_indent( "Malware scan: issuing %s new-style remote scan (zINSTREAM)\n", scanner_name); +#if defined(EXIM_TCP_CORK) + (void) setsockopt(malware_daemon_ctx.sock, IPPROTO_TCP, EXIM_TCP_CORK, + US &on, sizeof(on)); +#endif /* Pass the string to ClamAV (10 = "zINSTREAM\0"), if not already sent */ if (cmd_str.len) - if (send(sock, cmd_str.data, cmd_str.len, 0) < 0) + 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); + malware_daemon_ctx.sock); - /* calc file size */ - if ((clam_fd = open(CS eml_filename, O_RDONLY)) < 0) + 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)), - sock); + malware_daemon_ctx.sock); } - if ((fsize = lseek(clam_fd, 0, SEEK_END)) < 0) + if (fstat(clam_fd, &st) < 0) { - int err; -b_seek: err = errno; + int err = errno; (void)close(clam_fd); return m_panic_defer_3(scanent, NULL, - string_sprintf("can't seek spool file %s: %s", + string_sprintf("can't stat spool file %s: %s", eml_filename, strerror(err)), - sock); + malware_daemon_ctx.sock); } - fsize_uint = (unsigned int) fsize; - if ((off_t)fsize_uint != fsize) + fsize_uint = (unsigned int) st.st_size; + if ((off_t)fsize_uint != st.st_size) { (void)close(clam_fd); return m_panic_defer_3(scanent, NULL, - string_sprintf("seeking spool file %s, size overflow", - eml_filename), - sock); + string_sprintf("stat spool file %s, size overflow", eml_filename), + malware_daemon_ctx.sock); } - if (lseek(clam_fd, 0, SEEK_SET) < 0) - goto b_seek; - if (!(clamav_fbuf = US malloc(fsize_uint))) - { - (void)close(clam_fd); + /* send file size */ + send_size = htonl(fsize_uint); + if (send(malware_daemon_ctx.sock, &send_size, sizeof(send_size), 0) < 0) return m_panic_defer_3(scanent, NULL, - string_sprintf("unable to allocate memory %u for file (%s)", - fsize_uint, eml_filename), - sock); - } + string_sprintf("unable to send file size to socket (%s)", hostname), + malware_daemon_ctx.sock); - if ((result = read(clam_fd, clamav_fbuf, fsize_uint)) < 0) + /* send file body */ + while (fsize_uint) { - int err = errno; - 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)), - sock); +#ifdef OS_SENDFILE + int n = os_sendfile(malware_daemon_ctx.sock, clam_fd, NULL, (size_t)fsize_uint); + if (n < 0) + return m_panic_defer_3(scanent, NULL, + string_sprintf("unable to send file body to socket (%s): %s", hostname, strerror(errno)), + malware_daemon_ctx.sock); + fsize_uint -= n; +#else + int n = MIN(fsize_uint, big_buffer_size); + if ((n = read(clam_fd, big_buffer, n)) < 0) + return m_panic_defer_3(scanent, NULL, + string_sprintf("can't read spool file %s: %s", + eml_filename, strerror(errno)), + malware_daemon_ctx.sock); + if (send(malware_daemon_ctx.sock, big_buffer, (size_t)n, 0) < 0) + return m_panic_defer_3(scanent, NULL, + string_sprintf("unable to send file body to socket (%s): %s", hostname, strerror(errno)), + malware_daemon_ctx.sock); + fsize_uint -= n; +# ifdef EXIM_TCP_CORK + if (corked) + { + corked = FALSE; + (void) setsockopt(malware_daemon_ctx.sock, IPPROTO_TCP, EXIM_TCP_CORK, + US &off, sizeof(off)); + } +# endif +#endif /*!OS_SENDFILE*/ + } - (void)close(clam_fd); - /* send file body to socket */ - 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); + if (send(malware_daemon_ctx.sock, &send_final_zeroblock, sizeof(send_final_zeroblock), 0) < 0) return m_panic_defer_3(scanent, NULL, - string_sprintf("unable to send file body to socket (%s)", hostname), - sock); - } - - free(clamav_fbuf); + string_sprintf("unable to send file terminator to socket (%s)", hostname), + malware_daemon_ctx.sock); +#ifdef OS_SENDFILE + (void) setsockopt(malware_daemon_ctx.sock, IPPROTO_TCP, EXIM_TCP_CORK, + US &off, sizeof(off)); +#endif } else { /* use scan command */ /* Send a SCAN command pointing to a filename; then in the then in the - * scan-method-neutral part, read the response back */ + scan-method-neutral part, read the response back */ /* ================================================================= */ @@ -1684,22 +1754,23 @@ b_seek: err = errno; scanner_name, scanner_options); if (cmd_str.len) - if (send(sock, cmd_str.data, cmd_str.len, 0) < 0) + 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)), - sock); + malware_daemon_ctx.sock); /* Do not shut down the socket for writing; a user report noted that - * clamd 0.70 does not react well to this. */ + clamd 0.70 does not react well to this. */ } /* Commands have been sent, no matter which scan method or connection - * type we're using; now just read the result, independent of method. */ + type we're using; now just read the result, independent of method. */ /* Read the result */ memset(av_buffer, 0, sizeof(av_buffer)); - bread = ip_recv(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL)); - (void)close(sock); - sock = -1; + 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, @@ -1738,6 +1809,7 @@ b_seek: err = errno; /* strip newline at the end (won't be present for zINSTREAM) (also any trailing whitespace, which shouldn't exist, but we depend upon this below, so double-check) */ + p = av_buffer + Ustrlen(av_buffer) - 1; if (*p == '\n') *p = '\0'; @@ -1812,8 +1884,8 @@ b_seek: err = errno; uschar * linebuffer; uschar * sockline_scanner; uschar sockline_scanner_default[] = "%s\n"; - const pcre *sockline_trig_re; - const pcre *sockline_name_re; + const pcre2_code *sockline_trig_re; + const pcre2_code *sockline_name_re; /* find scanner command line */ if ( (sockline_scanner = string_nextinlist(&av_scanner_work, &sep, @@ -1825,7 +1897,7 @@ b_seek: err = errno; if (s++) if ((*s != 's' && *s != '%') || Ustrchr(s+1, '%')) return m_panic_defer_3(scanent, NULL, - US"unsafe sock scanner call spec", sock); + US"unsafe sock scanner call spec", malware_daemon_ctx.sock); } else sockline_scanner = sockline_scanner_default; @@ -1836,13 +1908,13 @@ b_seek: err = errno; 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, sock); + 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, sock); + 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); @@ -1850,20 +1922,20 @@ b_seek: err = errno; string_printing(commandline)); /* Pass the command string to the socket */ - if (m_sock_send(sock, commandline, Ustrlen(commandline), &errstr) < 0) + 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(sock, av_buffer, sizeof(av_buffer), tmo-time(NULL)); + 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)), - sock); + malware_daemon_ctx.sock); if (bread == sizeof(av_buffer)) return m_panic_defer_3(scanent, CUS callout_address, - US"buffer too small", sock); + 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: ", @@ -1902,16 +1974,16 @@ b_seek: err = errno; string_sprintf("invalid option '%s'", scanner_options)); } - if((sock = ip_unixsocket(US "/var/run/mksd/socket", &errstr)) < 0) + 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; 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) + if ((retval = mksd_scan_packed(scanent, malware_daemon_ctx.sock, eml_filename, tmo)) != OK) { - close (sock); + close (malware_daemon_ctx.sock); return retval; } break; @@ -1921,14 +1993,13 @@ b_seek: err = errno; #ifndef DISABLE_MAL_AVAST case M_AVAST: /* "avast" scanner type ----------------------------------- */ { - int ovector[3*3]; uschar buf[1024]; uschar * scanrequest; enum {AVA_HELO, AVA_OPT, AVA_RSP, AVA_DONE} avast_stage; int nread; - int more_data; uschar * error_message = NULL; - int strict = TRUE; + BOOL more_data = FALSE; + BOOL strict = TRUE; /* According to Martin Tuma @avast the protocol uses "escaped whitespace", that is, every embedded whitespace is backslash @@ -1937,7 +2008,7 @@ b_seek: err = errno; and the [ ] marker. [+] - not infected [L] - infected - [E] - some error occured + [E] - some error occurred Such marker follows the first non-escaped TAB. For more information see avast-protocol(5) @@ -1981,7 +2052,7 @@ b_seek: err = errno; /* wait for result */ for (avast_stage = AVA_HELO; - (nread = recv_line(sock, buf, sizeof(buf), tmo)) > 0; + (nread = recv_line(malware_daemon_ctx.sock, buf, sizeof(buf), tmo)) > 0; ) { int slen = Ustrlen(buf); @@ -2035,41 +2106,28 @@ b_seek: err = errno; /* send config-cmd or scan-request to socket */ len = Ustrlen(scanrequest); - if (send(sock, scanrequest, len, 0) == -1) + if (send(malware_daemon_ctx.sock, scanrequest, len, 0) == -1) { scanrequest[len-1] = '\0'; 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 (isdigit(buf[0])) - { /* we're done, this is the last response line from the scanner */ - DEBUG(D_acl) debug_printf_indent("sent to avast QUIT\n"); - if (send(sock, "QUIT\n", 5, 0) == -1) /* courtesy */ - return m_panic_defer_3(scanent, CUS callout_address, - string_sprintf( - "unable to send quit request to socket (%s): %s", - scanner_options, strerror(errno)), - sock); - - if (buf[0] != '2') error_message = buf; - avast_stage = AVA_DONE; + if (isdigit(buf[0])) /* We're done */ goto endloop; - } - if (malware_name) /* nothing else matters, just read on */ + if (malware_name) /* Nothing else matters, just read on */ break; - if (pcre_exec(ava_re_clean, NULL, CS buf, slen, - 0, 0, ovector, nelem(ovector)) > 0) + if (regex_match(ava_re_clean, buf, slen, NULL)) break; - if (malware_name = m_pcre_exec(ava_re_virus, buf)) + if ((malware_name = m_pcre_exec(ava_re_virus, buf))) { unescape(malware_name); DEBUG(D_acl) @@ -2077,15 +2135,17 @@ b_seek: err = errno; break; } - if (strict && (malware_name = m_pcre_exec(ava_re_error, buf))) + if (strict) /* treat scanner errors as malware */ { - unescape(malware_name); - DEBUG(D_acl) - debug_printf_indent("unescaped error message: '%s'\n", malware_name); - break; + 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; + } } - - if (pcre_exec(ava_re_error, NULL, CS buf, slen, 0, 0, NULL, 0) == 0) + else if (regex_match(ava_re_error, buf, slen, NULL)) { log_write(0, LOG_MAIN, "internal scanner error (ignored): %s", buf); break; @@ -2093,7 +2153,7 @@ b_seek: err = errno; /* here also for any unexpected response from the scanner */ DEBUG(D_acl) debug_printf("avast response not handled: '%s'\n", buf); - error_message = string_sprintf(string_sprintf("unexpected response from scanner: '%s'", buf)); + goto endloop; default: log_write(0, LOG_PANIC, "%s:%d:%s: should not happen", @@ -2101,29 +2161,29 @@ b_seek: err = errno; } } } + endloop: - switch(avast_stage) - { - case AVA_HELO: - case AVA_OPT: - case AVA_RSP: - if (nread == -1) error_message = "EOF from scanner"; - else if (nread < 0) error_message = "timeout from scanner"; + 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; - case AVA_DONE: - if (error_message) - return m_panic_defer_3(scanent, CUS callout_address, error_message, sock); + 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); - default: break; - } - break; } #endif } /* scanner type switch */ - if (sock >= 0) - (void) close (sock); + if (malware_daemon_ctx.sock >= 0) + (void) close (malware_daemon_ctx.sock); malware_ok = TRUE; /* set "been here, done that" marker */ } @@ -2194,7 +2254,7 @@ 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; +f.enable_dollar_recipients = TRUE; ret = malware_internal(US"*", eml_filename, 0); @@ -2250,13 +2310,13 @@ if (!fprot6d_re_virus) } -void -malware_show_supported(FILE * f) +gstring * +malware_show_supported(gstring * g) { -struct scan * sc; -fprintf(f, "Malware:"); -for (sc = m_scans; sc->scancode != -1; sc++) fprintf(f, " %s", sc->name); -fprintf(f, "\n"); +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"); }