/* returns * -1 on fatal error (shutdown) * 0 on ok * >0 errors encountered */ int command(client_conn_t *conn, int *virus) { int desc = conn->sd; struct cl_engine *engine = conn->engine; unsigned int options = conn->options; const struct optstruct *opts = conn->opts; int type = -1; /* TODO: make this enum */ int maxdirrec; int ret = 0; int flags = CLI_FTW_STD; struct scan_cb_data scandata; struct cli_ftw_cbdata data; unsigned ok, error, total; STATBUF sb; jobgroup_t *group = NULL; if (thrmgr_group_need_terminate(conn->group)) { logg("$Client disconnected while command was active\n"); if (conn->scanfd != -1) close(conn->scanfd); return 1; } thrmgr_setactiveengine(engine); data.data = &scandata; memset(&scandata, 0, sizeof(scandata)); scandata.id = conn->id; scandata.group = conn->group; scandata.odesc = desc; scandata.conn = conn; scandata.options = options; scandata.engine = engine; scandata.opts = opts; scandata.thr_pool = conn->thrpool; scandata.toplevel_path = conn->filename; switch (conn->cmdtype) { case COMMAND_SCAN: thrmgr_setactivetask(NULL, "SCAN"); type = TYPE_SCAN; break; case COMMAND_CONTSCAN: thrmgr_setactivetask(NULL, "CONTSCAN"); type = TYPE_CONTSCAN; break; case COMMAND_MULTISCAN: { int multiscan, max, alive; /* use MULTISCAN only for directories (bb #1869) */ if (STAT(conn->filename, &sb) == 0 && !S_ISDIR(sb.st_mode)) { thrmgr_setactivetask(NULL, "CONTSCAN"); type = TYPE_CONTSCAN; break; } pthread_mutex_lock(&conn->thrpool->pool_mutex); multiscan = conn->thrpool->thr_multiscan; max = conn->thrpool->thr_max; if (multiscan+1 < max) conn->thrpool->thr_multiscan = multiscan+1; else { alive = conn->thrpool->thr_alive; ret = -1; } pthread_mutex_unlock(&conn->thrpool->pool_mutex); if (ret) { /* multiscan has 1 control thread, so there needs to be at least 1 threads that is a non-multiscan controlthread to scan and make progress. */ logg("^Not enough threads for multiscan. Max: %d, Alive: %d, Multiscan: %d+1\n", max, alive, multiscan); conn_reply(conn, conn->filename, "Not enough threads for multiscan. Increase MaxThreads.", "ERROR"); return 1; } flags &= ~CLI_FTW_NEED_STAT; thrmgr_setactivetask(NULL, "MULTISCAN"); type = TYPE_MULTISCAN; scandata.group = group = thrmgr_group_new(); if (!group) { if(optget(opts, "ExitOnOOM")->enabled) return -1; else return 1; } break; } case COMMAND_MULTISCANFILE: thrmgr_setactivetask(NULL, "MULTISCANFILE"); scandata.group = NULL; scandata.type = TYPE_SCAN; scandata.thr_pool = NULL; /* TODO: check ret value */ ret = scan_callback(NULL, conn->filename, conn->filename, visit_file, &data); /* callback freed it */ conn->filename = NULL; *virus = scandata.infected; if (ret == CL_BREAK) { thrmgr_group_terminate(conn->group); return 1; } return scandata.errors > 0 ? scandata.errors : 0; case COMMAND_FILDES: thrmgr_setactivetask(NULL, "FILDES"); #ifdef HAVE_FD_PASSING if (conn->scanfd == -1) { conn_reply_error(conn, "FILDES: didn't receive file descriptor."); return 1; } else { ret = scanfd(conn, NULL, engine, options, opts, desc, 0); if (ret == CL_VIRUS) { *virus = 1; ret = 0; } else if (ret == CL_EMEM) { if(optget(opts, "ExitOnOOM")->enabled) ret = -1; else ret = 1; } else if (ret == CL_ETIMEOUT) { thrmgr_group_terminate(conn->group); ret = 1; } else ret = 0; logg("$Closed fd %d\n", conn->scanfd); close(conn->scanfd); } return ret; #else conn_reply_error(conn, "FILDES support not compiled in."); close(conn->scanfd); return 0; #endif case COMMAND_STATS: thrmgr_setactivetask(NULL, "STATS"); if (conn->group) mdprintf(desc, "%u: ", conn->id); thrmgr_printstats(desc, conn->term); return 0; case COMMAND_STREAM: thrmgr_setactivetask(NULL, "STREAM"); ret = scanstream(desc, NULL, engine, options, opts, conn->term); if (ret == CL_VIRUS) *virus = 1; if (ret == CL_EMEM) { if(optget(opts, "ExitOnOOM")->enabled) return -1; else return 1; } return 0; case COMMAND_INSTREAMSCAN: thrmgr_setactivetask(NULL, "INSTREAM"); ret = scanfd(conn, NULL, engine, options, opts, desc, 1); if (ret == CL_VIRUS) { *virus = 1; ret = 0; } else if (ret == CL_EMEM) { if(optget(opts, "ExitOnOOM")->enabled) ret = -1; else ret = 1; } else if (ret == CL_ETIMEOUT) { thrmgr_group_terminate(conn->group); ret = 1; } else ret = 0; if (ftruncate(conn->scanfd, 0) == -1) { /* not serious, we're going to close it and unlink it anyway */ logg("*ftruncate failed: %d\n", errno); } close(conn->scanfd); conn->scanfd = -1; cli_unlink(conn->filename); return ret; default: logg("!Invalid command distpached: %d\n", conn->cmdtype); return 1; } scandata.type = type; maxdirrec = optget(opts, "MaxDirectoryRecursion")->numarg; if (optget(opts, "FollowDirectorySymlinks")->enabled) flags |= CLI_FTW_FOLLOW_DIR_SYMLINK; if (optget(opts, "FollowFileSymlinks")->enabled) flags |= CLI_FTW_FOLLOW_FILE_SYMLINK; if(!optget(opts, "CrossFilesystems")->enabled) if(STAT(conn->filename, &sb) == 0) scandata.dev = sb.st_dev; ret = cli_ftw(conn->filename, flags, maxdirrec ? maxdirrec : INT_MAX, scan_callback, &data, scan_pathchk); if (ret == CL_EMEM) { if(optget(opts, "ExitOnOOM")->enabled) return -1; else return 1; } if (scandata.group && type == TYPE_MULTISCAN) { thrmgr_group_waitforall(group, &ok, &error, &total); pthread_mutex_lock(&conn->thrpool->pool_mutex); conn->thrpool->thr_multiscan--; pthread_mutex_unlock(&conn->thrpool->pool_mutex); } else { error = scandata.errors; total = scandata.total; ok = total - error - scandata.infected; } if (ok + error == total && (error != total)) { if (conn_reply_single(conn, conn->filename, "OK") == -1) ret = CL_ETIMEOUT; } *virus = total - (ok + error); if (ret == CL_ETIMEOUT) thrmgr_group_terminate(conn->group); return error; }
int scanfd(const client_conn_t *conn, unsigned long int *scanned, const struct cl_engine *engine, unsigned int options, const struct optstruct *opts, int odesc, int stream) { int ret, fd = conn->scanfd; const char *virname; STATBUF statbuf; struct cb_context context; char fdstr[32]; const char*reply_fdstr; UNUSEDPARAM(odesc); if (stream) { struct sockaddr_in sa; socklen_t salen = sizeof(sa); if(getpeername(conn->sd, (struct sockaddr *)&sa, &salen) || salen > sizeof(sa) || sa.sin_family != AF_INET) strncpy(fdstr, "instream(local)", sizeof(fdstr)); else snprintf(fdstr, sizeof(fdstr), "instream(%s@%u)", inet_ntoa(sa.sin_addr), ntohs(sa.sin_port)); reply_fdstr = "stream"; } else { snprintf(fdstr, sizeof(fdstr), "fd[%d]", fd); reply_fdstr = fdstr; } if(FSTAT(fd, &statbuf) == -1 || !S_ISREG(statbuf.st_mode)) { logg("%s: Not a regular file. ERROR\n", fdstr); if (conn_reply(conn, reply_fdstr, "Not a regular file", "ERROR") == -1) return CL_ETIMEOUT; return -1; } thrmgr_setactivetask(fdstr, NULL); context.filename = fdstr; context.virsize = 0; context.scandata = NULL; ret = cl_scandesc_callback(fd, &virname, scanned, engine, options, &context); thrmgr_setactivetask(NULL, NULL); if (thrmgr_group_need_terminate(conn->group)) { logg("*Client disconnected while scanjob was active\n"); return ret == CL_ETIMEOUT ? ret : CL_BREAK; } if(ret == CL_VIRUS) { if (conn_reply_virus(conn, reply_fdstr, virname) == -1) ret = CL_ETIMEOUT; if(context.virsize && optget(opts, "ExtendedDetectionInfo")->enabled) logg("%s: %s(%s:%llu) FOUND\n", fdstr, virname, context.virhash, context.virsize); else logg("%s: %s FOUND\n", fdstr, virname); virusaction(reply_fdstr, virname, opts); } else if(ret != CL_CLEAN) { if (conn_reply(conn, reply_fdstr, cl_strerror(ret), "ERROR") == -1) ret = CL_ETIMEOUT; logg("%s: %s ERROR\n", fdstr, cl_strerror(ret)); } else { if (conn_reply_single(conn, reply_fdstr, "OK") == CL_ETIMEOUT) ret = CL_ETIMEOUT; if(logok) logg("%s: OK\n", fdstr); } return ret; }
static const char* parse_dispatch_cmd(client_conn_t *conn, struct fd_buf *buf, size_t *ppos, int *error, const struct optstruct *opts, int readtimeout) { const char *cmd = NULL; int rc; size_t cmdlen; char term; int oldstyle; size_t pos = *ppos; /* Parse & dispatch commands */ while ((conn->mode == MODE_COMMAND) && (cmd = get_cmd(buf, pos, &cmdlen, &term, &oldstyle)) != NULL) { const char *argument; enum commands cmdtype; if (conn->group && oldstyle) { logg("$Received oldstyle command inside IDSESSION: %s\n", cmd); conn_reply_error(conn, "Only nCMDS\\n and zCMDS\\0 are accepted inside IDSESSION."); *error = 1; break; } cmdtype = parse_command(cmd, &argument, oldstyle); logg("$got command %s (%u, %u), argument: %s\n", cmd, (unsigned)cmdlen, (unsigned)cmdtype, argument ? argument : ""); if (cmdtype == COMMAND_FILDES) { if (buf->buffer + buf->off <= cmd + strlen("FILDES\n")) { /* we need the extra byte from recvmsg */ conn->mode = MODE_WAITANCILL; buf->mode = MODE_WAITANCILL; /* put term back */ buf->buffer[pos + cmdlen] = term; cmdlen = 0; logg("$RECVTH: mode -> MODE_WAITANCILL\n"); break; } /* eat extra \0 for controlmsg */ cmdlen++; logg("$RECVTH: FILDES command complete\n"); } conn->term = term; buf->term = term; if ((rc = execute_or_dispatch_command(conn, cmdtype, argument)) < 0) { logg("!Command dispatch failed\n"); if(rc == -1 && optget(opts, "ExitOnOOM")->enabled) { pthread_mutex_lock(&exit_mutex); progexit = 1; pthread_mutex_unlock(&exit_mutex); } *error = 1; } if (thrmgr_group_need_terminate(conn->group)) { logg("$Receive thread: have to terminate group\n"); *error = CL_ETIMEOUT; break; } if (*error || !conn->group || rc) { if (rc && thrmgr_group_finished(conn->group, EXIT_OK)) { logg("$Receive thread: closing conn (FD %d), group finished\n", conn->sd); /* if there are no more active jobs */ shutdown(conn->sd, 2); closesocket(conn->sd); buf->fd = -1; conn->group = NULL; } else if (conn->mode != MODE_STREAM) { logg("$mode -> MODE_WAITREPLY\n"); /* no more commands are accepted */ conn->mode = MODE_WAITREPLY; /* Stop monitoring this FD, it will be closed either * by us, or by the scanner thread. * Never close a file descriptor that is being * monitored by poll()/select() from another thread, * because this can lead to subtle bugs such as: * Other thread closes file descriptor -> POLLHUP is * set, but the poller thread doesn't wake up yet. * Another client opens a connection and sends some * data. If the socket reuses the previous file descriptor, * then POLLIN is set on the file descriptor too. * When poll() wakes up it sees POLLIN | POLLHUP * and thinks that the client has sent some data, * and closed the connection, so clamd closes the * connection in turn resulting in a bug. * * If we wouldn't have poll()-ed the file descriptor * we closed in another thread, but rather made sure * that we don't put a FD that we're about to close * into poll()'s list of watched fds; then POLLHUP * would be set, but the file descriptor would stay * open, until we wake up from poll() and close it. * Thus a new connection won't be able to reuse the * same FD, and there is no bug. * */ buf->fd = -1; } } /* we received a command, set readtimeout */ time(&buf->timeout_at); buf->timeout_at += readtimeout; pos += cmdlen+1; if (conn->mode == MODE_STREAM) { /* TODO: this doesn't belong here */ buf->dumpname = conn->filename; buf->dumpfd = conn->scanfd; logg("$Receive thread: INSTREAM: %s fd %u\n", buf->dumpname, buf->dumpfd); } if (conn->mode != MODE_COMMAND) { logg("$Breaking command loop, mode is no longer MODE_COMMAND\n"); break; } conn->id++; } *ppos = pos; buf->mode = conn->mode; buf->id = conn->id; buf->group = conn->group; buf->quota = conn->quota; if (conn->scanfd != -1 && conn->scanfd != buf->dumpfd) { logg("$Unclaimed file descriptor received, closing: %d\n", conn->scanfd); close(conn->scanfd); /* protocol error */ conn_reply_error(conn, "PROTOCOL ERROR: ancillary data sent without FILDES."); *error = 1; return NULL; } if (!*error) { /* move partial command to beginning of buffer */ if (pos < buf->off) { memmove (buf->buffer, &buf->buffer[pos], buf->off - pos); buf->off -= pos; } else buf->off = 0; if (buf->off) logg("$Moved partial command: %lu\n", (unsigned long)buf->off); else logg("$Consumed entire command\n"); /* adjust pos to account for the buffer shuffle */ pos = 0; } *ppos = pos; return cmd; }
int scan_callback(STATBUF *sb, char *filename, const char *msg, enum cli_ftw_reason reason, struct cli_ftw_cbdata *data) { struct scan_cb_data *scandata = data->data; const char *virname = NULL; int ret; int type = scandata->type; struct cb_context context; /* detect disconnected socket, * this should NOT detect half-shutdown sockets (SHUT_WR) */ if (send(scandata->conn->sd, &ret, 0, 0) == -1 && errno != EINTR) { logg("$Client disconnected while command was active!\n"); thrmgr_group_terminate(scandata->conn->group); if (reason == visit_file) free(filename); return CL_BREAK; } if (thrmgr_group_need_terminate(scandata->conn->group)) { logg("^Client disconnected while scanjob was active\n"); if (reason == visit_file) free(filename); return CL_BREAK; } scandata->total++; switch (reason) { case error_mem: if (msg) logg("!Memory allocation failed during cli_ftw() on %s\n", msg); else logg("!Memory allocation failed during cli_ftw()\n"); scandata->errors++; return CL_EMEM; case error_stat: conn_reply_errno(scandata->conn, msg, "lstat() failed:"); logg("^lstat() failed on: %s\n", msg); scandata->errors++; return CL_SUCCESS; case warning_skipped_dir: logg("^Directory recursion limit reached, skipping %s\n", msg); return CL_SUCCESS; case warning_skipped_link: logg("$Skipping symlink: %s\n", msg); return CL_SUCCESS; case warning_skipped_special: if (msg == scandata->toplevel_path) conn_reply(scandata->conn, msg, "Not supported file type", "ERROR"); logg("*Not supported file type: %s\n", msg); return CL_SUCCESS; case visit_directory_toplev: return CL_SUCCESS; case visit_file: break; } /* check whether the file is excluded */ #ifdef C_LINUX if(procdev && sb && (sb->st_dev == procdev)) { free(filename); return CL_SUCCESS; } #endif if(sb && sb->st_size == 0) { /* empty file */ if (msg == scandata->toplevel_path) conn_reply_single(scandata->conn, filename, "Empty file"); free(filename); return CL_SUCCESS; } if (type == TYPE_MULTISCAN) { client_conn_t *client_conn = (client_conn_t *) calloc(1, sizeof(struct client_conn_tag)); if(client_conn) { client_conn->scanfd = -1; client_conn->sd = scandata->odesc; client_conn->filename = filename; client_conn->cmdtype = COMMAND_MULTISCANFILE; client_conn->term = scandata->conn->term; client_conn->options = scandata->options; client_conn->opts = scandata->opts; client_conn->group = scandata->group; if(cl_engine_addref(scandata->engine)) { logg("!cl_engine_addref() failed\n"); free(filename); free(client_conn); return CL_EMEM; } else { client_conn->engine = scandata->engine; pthread_mutex_lock(&reload_mutex); client_conn->engine_timestamp = reloaded_time; pthread_mutex_unlock(&reload_mutex); if(!thrmgr_group_dispatch(scandata->thr_pool, scandata->group, client_conn, 1)) { logg("!thread dispatch failed\n"); cl_engine_free(scandata->engine); free(filename); free(client_conn); return CL_EMEM; } } } else { logg("!Can't allocate memory for client_conn\n"); scandata->errors++; free(filename); return CL_EMEM; } return CL_SUCCESS; } if (access(filename, R_OK)) { if (conn_reply(scandata->conn, filename, "Access denied.", "ERROR") == -1) { free(filename); return CL_ETIMEOUT; } logg("*Access denied: %s\n", filename); scandata->errors++; free(filename); return CL_SUCCESS; } thrmgr_setactivetask(filename, NULL); context.filename = filename; context.virsize = 0; context.scandata = scandata; ret = cl_scanfile_callback(filename, &virname, &scandata->scanned, scandata->engine, scandata->options, &context); thrmgr_setactivetask(NULL, NULL); if (thrmgr_group_need_terminate(scandata->conn->group)) { free(filename); logg("*Client disconnected while scanjob was active\n"); return ret == CL_ETIMEOUT ? ret : CL_BREAK; } if ((ret == CL_VIRUS) && (virname == NULL)) { logg("*%s: reported CL_VIRUS but no virname returned!\n", filename); ret = CL_EMEM; } if (ret == CL_VIRUS) { scandata->infected++; if (scandata->options & CL_SCAN_ALLMATCHES) { if(optget(scandata->opts, "PreludeEnable")->enabled){ prelude_logging(filename, virname, context.virhash, context.virsize); } virusaction(filename, virname, scandata->opts); } else { if (conn_reply_virus(scandata->conn, filename, virname) == -1) { free(filename); return CL_ETIMEOUT; } if(optget(scandata->opts, "PreludeEnable")->enabled){ prelude_logging(filename, virname, context.virhash, context.virsize); } if(context.virsize && optget(scandata->opts, "ExtendedDetectionInfo")->enabled) logg("~%s: %s(%s:%llu) FOUND\n", filename, virname, context.virhash, context.virsize); else logg("~%s: %s FOUND\n", filename, virname); virusaction(filename, virname, scandata->opts); } } else if (ret != CL_CLEAN) { scandata->errors++; if (conn_reply(scandata->conn, filename, cl_strerror(ret), "ERROR") == -1) { free(filename); return CL_ETIMEOUT; } logg("~%s: %s ERROR\n", filename, cl_strerror(ret)); } else if (logok) { logg("~%s: OK\n", filename); } free(filename); if(ret == CL_EMEM) /* stop scanning */ return ret; if (type == TYPE_SCAN) { /* virus -> break */ return ret; } /* keep scanning always */ return CL_SUCCESS; }
/* returns * -1 on fatal error (shutdown) * 0 on ok * >0 errors encountered */ int command(client_conn_t *conn, int *virus) { int desc = conn->sd; struct cl_engine *engine = conn->engine; unsigned int options = conn->options; const struct optstruct *opts = conn->opts; int type = -1; /* TODO: make this enum */ int maxdirrec; int ret = 0; int flags = CLI_FTW_STD; struct scan_cb_data scandata; struct cli_ftw_cbdata data; unsigned ok, error, total; jobgroup_t *group = NULL; if (thrmgr_group_need_terminate(conn->group)) { logg("$Client disconnected while command was active\n"); if (conn->scanfd != -1) close(conn->scanfd); return 1; } thrmgr_setactiveengine(engine); data.data = &scandata; memset(&scandata, 0, sizeof(scandata)); scandata.id = conn->id; scandata.group = conn->group; scandata.odesc = desc; scandata.conn = conn; scandata.options = options; scandata.engine = engine; scandata.opts = opts; scandata.thr_pool = conn->thrpool; scandata.toplevel_path = conn->filename; switch (conn->cmdtype) { case COMMAND_SCAN: thrmgr_setactivetask(NULL, "SCAN"); type = TYPE_SCAN; break; case COMMAND_CONTSCAN: thrmgr_setactivetask(NULL, "CONTSCAN"); type = TYPE_CONTSCAN; break; case COMMAND_MULTISCAN: flags &= ~CLI_FTW_NEED_STAT; thrmgr_setactivetask(NULL, "MULTISCAN"); type = TYPE_MULTISCAN; scandata.group = group = thrmgr_group_new(); if (!group) return CL_EMEM; break; case COMMAND_MULTISCANFILE: thrmgr_setactivetask(NULL, "MULTISCANFILE"); scandata.group = NULL; scandata.type = TYPE_SCAN; scandata.thr_pool = NULL; /* TODO: check ret value */ ret = scan_callback(NULL, conn->filename, conn->filename, visit_file, &data); /* callback freed it */ conn->filename = NULL; *virus = scandata.infected; if (ret == CL_BREAK) { thrmgr_group_terminate(conn->group); return CL_BREAK; } return scandata.errors > 0 ? scandata.errors : 0; case COMMAND_FILDES: thrmgr_setactivetask(NULL, "FILDES"); #ifdef HAVE_FD_PASSING if (conn->scanfd == -1) conn_reply_error(conn, "FILDES: didn't receive file descriptor."); else { ret = scanfd(conn->scanfd, conn, NULL, engine, options, opts, desc, 0); if (ret == CL_VIRUS) { *virus = 1; } else if (ret == CL_EMEM) { if(optget(opts, "ExitOnOOM")->enabled) ret = -1; } else if (ret == CL_ETIMEOUT) { thrmgr_group_terminate(conn->group); ret = 1; } else ret = 0; } logg("$Closed fd %d\n", conn->scanfd); close(conn->scanfd); return ret; #else conn_reply_error(conn, "FILDES support not compiled in."); close(conn->scanfd); return 0; #endif case COMMAND_STATS: thrmgr_setactivetask(NULL, "STATS"); if (conn->group) mdprintf(desc, "%u: ", conn->id); thrmgr_printstats(desc); return 0; case COMMAND_STREAM: thrmgr_setactivetask(NULL, "STREAM"); ret = scanstream(desc, NULL, engine, options, opts, conn->term); if (ret == CL_VIRUS) *virus = 1; if (ret == CL_EMEM) { if(optget(opts, "ExitOnOOM")->enabled) return -1; } return 0; case COMMAND_INSTREAMSCAN: thrmgr_setactivetask(NULL, "INSTREAM"); ret = scanfd(conn->scanfd, conn, NULL, engine, options, opts, desc, 1); if (ret == CL_VIRUS) { *virus = 1; } else if (ret == CL_EMEM) { if(optget(opts, "ExitOnOOM")->enabled) ret = -1; } else if (ret == CL_ETIMEOUT) { thrmgr_group_terminate(conn->group); ret = 1; } else ret = 0; if (ftruncate(conn->scanfd, 0) == -1) { /* not serious, we're going to close it and unlink it anyway */ logg("*ftruncate failed: %d\n", errno); } close(conn->scanfd); conn->scanfd = -1; cli_unlink(conn->filename); return ret; } scandata.type = type; maxdirrec = optget(opts, "MaxDirectoryRecursion")->numarg; if (optget(opts, "FollowDirectorySymlinks")->enabled) flags |= CLI_FTW_FOLLOW_DIR_SYMLINK; if (optget(opts, "FollowFileSymlinks")->enabled) flags |= CLI_FTW_FOLLOW_FILE_SYMLINK; ret = cli_ftw(conn->filename, flags, maxdirrec ? maxdirrec : INT_MAX, scan_callback, &data); if (ret == CL_EMEM) if(optget(opts, "ExitOnOOM")->enabled) return -1; if (scandata.group && conn->cmdtype == COMMAND_MULTISCAN) { thrmgr_group_waitforall(group, &ok, &error, &total); } else { error = scandata.errors; total = scandata.total; ok = total - error - scandata.infected; } if (ok + error == total && (error != total)) { if (conn_reply_single(conn, conn->filename, "OK") == -1) ret = CL_ETIMEOUT; } *virus = total - (ok + error); if (ret == CL_ETIMEOUT) thrmgr_group_terminate(conn->group); return error; }