unit_tests/check_clamd.c
904c4258
 /*
  *  Unit tests for clamd.
  *
  *  Copyright (C) 2009 Sourcefire, Inc.
  *
  *  Authors: Török Edvin
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License version 2 as
  *  published by the Free Software Foundation.
  *
  *  This program is distributed in the hope that it will be useful,
  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  *  GNU General Public License for more details.
  *
  *  You should have received a copy of the GNU General Public License
  *  along with this program; if not, write to the Free Software
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  *  MA 02110-1301, USA.
  */
05a213b8
 #if HAVE_CONFIG_H
 #include "clamav-config.h"
 #endif
1d60f70d
 #include "shared/fdpassing.h"
8cad8e83
 #include <arpa/inet.h>
770701eb
 #include <netinet/in.h>
05a213b8
 #include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
 #include <signal.h>
 #ifdef HAVE_SYS_TYPES_H
 #include <sys/types.h>
 #endif
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/un.h>
 #include <unistd.h>
 
5f6edb22
 #include <sys/time.h>
 #include <sys/resource.h>
 #include <sys/select.h>
 #include <sys/time.h>
 #include <sys/types.h>
 #include <unistd.h>
05a213b8
 #include <check.h>
 #include "checks_common.h"
 #include "libclamav/version.h"
 #include "libclamav/cltypes.h"
 
 #ifdef CHECK_HAVE_LOOPS
 
 static int sockd;
 #define SOCKET "clamd-test.socket"
73ba72eb
 static void conn_setup_mayfail(int may)
05a213b8
 {
     int rc;
     struct sockaddr_un nixsock;
     memset((void *)&nixsock, 0, sizeof(nixsock));
     nixsock.sun_family = AF_UNIX;
     strncpy(nixsock.sun_path, SOCKET, sizeof(nixsock.sun_path));
 
     sockd = socket(AF_UNIX, SOCK_STREAM, 0);
73ba72eb
     if (sockd == -1 && (may && (errno == EMFILE || errno == ENFILE)))
 	return;
05a213b8
     fail_unless_fmt(sockd != -1, "Unable to create socket: %s\n", strerror(errno));
 
     rc = connect(sockd, (struct sockaddr *)&nixsock, sizeof(nixsock));
     fail_unless_fmt(rc != -1, "Unable to connect(): %s\n", strerror(errno));
 
     signal(SIGPIPE, SIG_IGN);
 }
 
73ba72eb
 static void conn_setup(void)
 {
     conn_setup_mayfail(0);
 }
 
e114b109
 static int conn_tcp(int port)
 {
     struct sockaddr_in server;
     int rc;
     int sd = socket(AF_INET, SOCK_STREAM, 0);
     fail_unless_fmt(sd != -1, "Unable to create socket: %s\n", strerror(errno));
 
     memset(&server, 0, sizeof(server));
     server.sin_family = AF_INET;
     server.sin_port = htons(port);
     server.sin_addr.s_addr = inet_addr("127.0.0.1");
 
     rc = connect(sd, (struct sockaddr *)&server, sizeof(server));
     fail_unless_fmt(rc != -1, "Unable to connect(): %s\n", strerror(errno));
     return sd;
 }
 
05a213b8
 static void conn_teardown(void)
 {
     if (sockd != -1)
 	close(sockd);
 }
 
 #ifndef REPO_VERSION
 #define REPO_VERSION VERSION
 #endif
 
 #define SCANFILE BUILDDIR"/../test/clam.exe"
 #define FOUNDREPLY SCANFILE": ClamAV-Test-File.UNOFFICIAL FOUND"
 
4e24a361
 /* some clean file */
 #define CLEANFILE SRCDIR"/Makefile.am"
 #define CLEANREPLY CLEANFILE": OK"
 #define UNKNOWN_REPLY "UNKNOWN COMMAND"
 
 #define NONEXISTENT "/nonexistent\vfilename"
b0e57898
 
4e24a361
 #define NONEXISTENT_REPLY NONEXISTENT": lstat() failed: No such file or directory. ERROR"
 
 #define ACCDENIED BUILDDIR"/accdenied"
 #define ACCDENIED_REPLY ACCDENIED": Access denied. ERROR"
47b8b6c2
 static int isroot = 0;
4e24a361
 static void commands_setup(void)
 {
     const char *nonempty = "NONEMPTYFILE";
     int fd = open(NONEXISTENT, O_RDONLY);
     if (fd != -1) close(fd);
     fail_unless(fd == -1, "Nonexistent file exists!\n");
 
     fd = open(ACCDENIED, O_CREAT | O_WRONLY, S_IWUSR);
     fail_unless_fmt(fd != -1,
 		    "Failed to create file for access denied tests: %s\n", strerror(errno));
 
 
     fail_unless_fmt(fchmod(fd,  S_IWUSR) != -1,
 		    "Failed to chmod: %s\n", strerror(errno));
     /* must not be empty file */
e9e0833f
     fail_unless_fmt((size_t)write(fd, nonempty, strlen(nonempty)) == strlen(nonempty),
4e24a361
 		    "Failed to write into testfile: %s\n", strerror(errno));
     close(fd);
47b8b6c2
 
     /* skip access denied tests when run as root, as root will ignore
      * permissions */
     if (!geteuid())
 	isroot = 1;
4e24a361
 }
 
 static void commands_teardown(void)
 {
 }
 
456e31a3
 #define VERSION_REPLY "ClamAV "REPO_VERSION""VERSION_SUFFIX
 
39b84970
 #define VCMDS_REPLY VERSION_REPLY"| COMMANDS: SCAN QUIT RELOAD PING CONTSCAN VERSIONCOMMANDS VERSION STREAM END SHUTDOWN MULTISCAN FILDES STATS IDSESSION INSTREAM"
47d403f8
 
 enum idsession_support {
     IDS_OK, /* accepted */
     IDS_REJECT,
     /* after sending this message, clamd will reply,  then accept 
      * no further commands, but still reply to all active commands */
     IDS_END /* the END command */
 };
 
05a213b8
 static struct basic_test {
     const char *command;
4e24a361
     const char *extra;
05a213b8
     const char *reply;
537292a7
     int support_old;
47b8b6c2
     int skiproot;
47d403f8
     enum idsession_support ids;
05a213b8
 } basic_tests[] = {
47d403f8
     {"PING", NULL, "PONG", 1, 0, IDS_OK},
     {"RELOAD", NULL, "RELOADING", 1, 0, IDS_REJECT},
     {"VERSION", NULL, VERSION_REPLY, 1, 0, IDS_OK},
     {"VERSIONCOMMANDS", NULL, VCMDS_REPLY, 0, 0, IDS_REJECT},
     {"SCAN "SCANFILE, NULL, FOUNDREPLY, 1, 0, IDS_OK},
     {"SCAN "CLEANFILE, NULL, CLEANREPLY, 1, 0, IDS_OK},
     {"CONTSCAN "SCANFILE, NULL, FOUNDREPLY, 1, 0, IDS_REJECT},
     {"CONTSCAN "CLEANFILE, NULL, CLEANREPLY, 1, 0, IDS_REJECT},
     {"MULTISCAN "SCANFILE, NULL, FOUNDREPLY, 1, 0, IDS_REJECT},
     {"MULTISCAN "CLEANFILE, NULL, CLEANREPLY, 1, 0, IDS_REJECT},
4e24a361
     /* unknown commnads */
47d403f8
     {"RANDOM", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
4e24a361
     /* commands invalid as first */
47d403f8
     {"END", NULL, UNKNOWN_REPLY, 1, 0, IDS_END},
4e24a361
     /* commands for nonexistent files */
47d403f8
     {"SCAN "NONEXISTENT, NULL, NONEXISTENT_REPLY, 1, 0, IDS_OK},
     {"CONTSCAN "NONEXISTENT, NULL, NONEXISTENT_REPLY, 1, 0, IDS_REJECT},
     {"MULTISCAN "NONEXISTENT, NULL, NONEXISTENT_REPLY, 1, 0, IDS_REJECT},
4e24a361
     /* commands for access denied files */
47d403f8
     {"SCAN "ACCDENIED, NULL, ACCDENIED_REPLY, 1, 1, IDS_OK},
     {"CONTSCAN "ACCDENIED, NULL, ACCDENIED_REPLY, 1, 1, IDS_REJECT},
     {"MULTISCAN "ACCDENIED, NULL, ACCDENIED_REPLY, 1, 1, IDS_REJECT},
4e24a361
     /* commands with invalid/missing arguments */
47d403f8
     {"SCAN", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
     {"CONTSCAN", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
     {"MULTISCAN", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
4e24a361
     /* commands with invalid data */
47d403f8
     {"INSTREAM", "\xff\xff\xff\xff", "INSTREAM size limit exceeded. ERROR", 0, 0, IDS_REJECT}, /* too big chunksize */
     {"FILDES", "X", "No file descriptor received. ERROR", 1, 0, IDS_REJECT}, /* FILDES w/o ancillary data */
05a213b8
 };
 
456e31a3
 static void *recvpartial(int sd, size_t *len, int partial)
05a213b8
 {
     char *buf = NULL;
     size_t off = 0;
     int rc;
 
     *len = 0;
     do {
        if (off + BUFSIZ > *len) {
 	    *len += BUFSIZ+1;
 	    buf = realloc(buf, *len);
 	    fail_unless(!!buf, "Cannot realloc buffer\n");
 	}
 	rc = recv(sd, buf + off, BUFSIZ, 0);
 	fail_unless_fmt(rc != -1, "recv() failed: %s\n", strerror(errno));
 	off += rc;
456e31a3
     } while (rc && (!partial || !memchr(buf, '\0', off)));
05a213b8
     *len = off;
ec868782
     buf[*len] = '\0';
05a213b8
     return buf;
 }
 
456e31a3
 static void *recvfull(int sd, size_t *len)
 {
     return recvpartial(sd, len, 0);
 }
 
4e24a361
 static void test_command(const char *cmd, size_t len, const char *extra, const char *expect, size_t expect_len)
05a213b8
 {
     void *recvdata;
8cad8e83
     ssize_t rc;
47b8b6c2
 
05a213b8
     rc = send(sockd, cmd, len, 0);
8cad8e83
     fail_unless_fmt((size_t)rc == len, "Unable to send(): %s\n", strerror(errno));
05a213b8
 
4e24a361
     if (extra) {
 	rc = send(sockd, extra, strlen(extra), 0);
0378a9ab
 	fail_unless_fmt((size_t)rc == strlen(extra), "Unable to send() extra for %s: %s\n", cmd, strerror(errno));
4e24a361
     }
7ff6ec03
     shutdown(sockd, SHUT_WR);
05a213b8
     recvdata = recvfull(sockd, &len);
 
ec868782
     fail_unless_fmt(len == expect_len, "Reply has wrong size: %lu, expected %lu, reply: %s, expected: %s\n",
 		    len, expect_len, recvdata, expect);
05a213b8
 
     rc = memcmp(recvdata, expect, expect_len);
     fail_unless_fmt(!rc, "Wrong reply for command %s: |%s|, expected: |%s|\n", cmd, recvdata, expect);
     free(recvdata);
7ff6ec03
 
05a213b8
 }
 
7bb82e6c
 #ifdef CHECK_HAVE_LOOPS
05a213b8
 START_TEST (test_basic_commands)
 {
     struct basic_test *test = &basic_tests[_i];
     char nsend[BUFSIZ], nreply[BUFSIZ];
 
47b8b6c2
     if (test->skiproot && isroot)
 	return;
05a213b8
     /* send nCOMMAND */
4e24a361
     snprintf(nreply, sizeof(nreply), "%s\n", test->reply);
05a213b8
     snprintf(nsend, sizeof(nsend), "n%s\n", test->command);
4e24a361
     conn_setup();
     test_command(nsend, strlen(nsend), test->extra, nreply, strlen(nreply));
05a213b8
     conn_teardown();
 
     /* send zCOMMAND */
8cad8e83
     snprintf(nsend, sizeof(nsend), "z%s", test->command);
4e24a361
     conn_setup();
     test_command(nsend, strlen(nsend)+1, test->extra, test->reply, strlen(test->reply)+1);
     conn_teardown();
 }
 END_TEST
 
 START_TEST (test_compat_commands)
 {
     /* test sending the command the "old way" */
     struct basic_test *test = &basic_tests[_i];
     char nsend[BUFSIZ], nreply[BUFSIZ];
 
47b8b6c2
     if (test->skiproot && isroot)
 	return;
 
537292a7
     if (!test->support_old) {
 	snprintf(nreply, sizeof(nreply), "UNKNOWN COMMAND\n");
4d26118b
 	test->extra = NULL;
537292a7
     } else {
 	snprintf(nreply, sizeof(nreply), "%s\n", test->reply);
     }
     /* one command = one packet, no delimiter */
4e24a361
     if (!test->extra) {
 	conn_setup();
 	test_command(test->command, strlen(test->command), test->extra, nreply, strlen(nreply));
 	conn_teardown();
     }
 
     /* one packet, \n delimited command, followed by "extra" if needed */
     snprintf(nsend, sizeof(nsend), "%s\n", test->command);
     conn_setup();
     test_command(nsend, strlen(nsend), test->extra, nreply, strlen(nreply));
     conn_teardown();
 
0378a9ab
     if (!test->extra) {
 	/* FILDES won't support this, because it expects
 	 * strlen("FILDES\n") characters, then 1 character and the FD. */
 	/* one packet, \r\n delimited command, followed by "extra" if needed */
 	snprintf(nsend, sizeof(nsend), "%s\r\n", test->command);
 	conn_setup();
 	test_command(nsend, strlen(nsend), test->extra, nreply, strlen(nreply));
 	conn_teardown();
     }
05a213b8
 }
 END_TEST
7bb82e6c
 #endif
05a213b8
 
 #define EXPECT_INSTREAM "stream: ClamAV-Test-File.UNOFFICIAL FOUND\n"
47d403f8
 #define EXPECT_INSTREAM0 "stream: ClamAV-Test-File.UNOFFICIAL FOUND"
05a213b8
 
6870ebe1
 #define STATS_REPLY "POOLS: 1\n\nSTATE: VALID PRIMARY\n"
ec868782
 START_TEST (test_stats)
6870ebe1
 {
     char *recvdata;
     size_t len = strlen("nSTATS\n");
     int rc;
 
     conn_setup();
     rc = send(sockd, "nSTATS\n", len, 0);
     fail_unless_fmt((size_t)rc == len, "Unable to send(): %s\n", strerror(errno));
 
     recvdata = recvfull(sockd, &len);
 
     fail_unless_fmt(len > strlen(STATS_REPLY), "Reply has wrong size: %lu, minimum %lu, reply: %s\n",
 		    len, strlen(STATS_REPLY), recvdata);
 
     if (len > strlen(STATS_REPLY))
 	len = strlen(STATS_REPLY);
     rc = strncmp(recvdata, STATS_REPLY, len);
 
     fail_unless_fmt(rc == 0, "Wrong reply: %s\n", recvdata);
     free(recvdata);
     conn_teardown();
 }
 END_TEST
 
47d403f8
 static size_t prepare_instream(char *buf, size_t off, size_t buflen)
05a213b8
 {
     struct stat stbuf;
47d403f8
     int fd, nread;
05a213b8
     uint32_t chunk;
     fail_unless_fmt(stat(SCANFILE, &stbuf) != -1, "stat failed for %s: %s", SCANFILE, strerror(errno));
 
     fd = open(SCANFILE, O_RDONLY);
     fail_unless_fmt(fd != -1, "open failed: %s\n", strerror(errno));
 
     chunk = htonl(stbuf.st_size);
     memcpy(&buf[off], &chunk, sizeof(chunk));
     off += 4;
47d403f8
     nread = read(fd, &buf[off], buflen-off-4);
     fail_unless_fmt(nread == stbuf.st_size, "read failed: %d != %d, %s\n", nread, stbuf.st_size, strerror(errno));
05a213b8
     off += nread;
     buf[off++]=0;
     buf[off++]=0;
     buf[off++]=0;
     buf[off++]=0;
     close(fd);
47d403f8
     return off;
 }
 
 START_TEST (test_instream)
 {
     void *recvdata;
     size_t len, expect_len;
     char buf[4096] = "nINSTREAM\n";
     size_t off = strlen(buf);
     int rc;
 
     off = prepare_instream(buf, off, sizeof(buf));
05a213b8
 
     conn_setup();
8cad8e83
     fail_unless((size_t)send(sockd, buf, off, 0) == off, "send() failed: %s\n", strerror(errno));
05a213b8
 
     recvdata = recvfull(sockd, &len);
 
     expect_len = strlen(EXPECT_INSTREAM);
     fail_unless_fmt(len == expect_len, "Reply has wrong size: %lu, expected %lu, reply: %s\n",
 		    len, expect_len, recvdata);
 
     rc = memcmp(recvdata, EXPECT_INSTREAM, expect_len);
     fail_unless_fmt(!rc, "Wrong reply for command INSTREAM: |%s|, expected: |%s|\n", recvdata, EXPECT_INSTREAM);
     free(recvdata);
 
     conn_teardown();
 }
 END_TEST
 
0378a9ab
 static int sendmsg_fd(int sockd, const char *mesg, size_t msg_len, int fd, int singlemsg)
ec868782
 {
     struct msghdr msg;
     struct cmsghdr *cmsg;
     unsigned char fdbuf[CMSG_SPACE(sizeof(int))];
0378a9ab
     char dummy[BUFSIZ];
ec868782
     struct iovec iov[1];
     int rc;
 
0378a9ab
     if (!singlemsg) {
 	/* send FILDES\n and then a single character + ancillary data */
 	dummy[0] = '\0';
 	iov[0].iov_base = dummy;
 	iov[0].iov_len = 1;
     } else {
 	/* send single message with ancillary data */
3e552ab6
 	fail_unless(msg_len < sizeof(dummy)-1, "message too large");
0378a9ab
 	memcpy(dummy, mesg, msg_len);
 	dummy[msg_len] = '\0';
 	iov[0].iov_base = dummy;
 	iov[0].iov_len = msg_len + 1;
     }
ec868782
 
     memset(&msg, 0, sizeof(msg));
     msg.msg_control = fdbuf;
     msg.msg_iov = iov;
     msg.msg_iovlen = 1;
     msg.msg_controllen = CMSG_LEN(sizeof(int));
 
     cmsg = CMSG_FIRSTHDR(&msg);
     cmsg->cmsg_len = CMSG_LEN(sizeof(int));
     cmsg->cmsg_level = SOL_SOCKET;
     cmsg->cmsg_type = SCM_RIGHTS;
     *(int *)CMSG_DATA(cmsg) = fd;
 
0378a9ab
     if (!singlemsg) {
 	rc = send(sockd, mesg, msg_len, 0);
 	if (rc == -1)
 	    return rc;
     }
 
     return sendmsg(sockd, &msg, 0);
 }
 
 static void tst_fildes(const char *cmd, size_t len, int fd,
 			const char *expect, size_t expect_len, int closefd, int singlemsg)
 {
     char *recvdata, *p;
     int rc;
ec868782
 
0378a9ab
     conn_setup();
     fail_unless_fmt(sendmsg_fd(sockd, cmd, len, fd, singlemsg) != -1,
 		     "Failed to sendmsg: %s\n", strerror(errno));
ec868782
 
     if (closefd)
 	close(fd);
 
     recvdata = recvfull(sockd, &len);
     p = strchr(recvdata, ':');
 
     fail_unless_fmt(!!p, "Reply doesn't contain ':' : %s\n", recvdata);
     *p++ = '\0';
 
     fail_unless_fmt(sscanf(recvdata, "fd[%u]", &rc) == 1, "Reply doesn't contain fd: %s\n", recvdata);
 
     len -= p - recvdata;
     fail_unless_fmt(len == expect_len, "Reply has wrong size: %lu, expected %lu, reply: %s, expected: %s\n",
 		    len, expect_len, p, expect);
 
     rc = memcmp(p, expect, expect_len);
     fail_unless_fmt(!rc, "Wrong reply for command %s: |%s|, expected: |%s|\n", cmd, p, expect);
     free(recvdata);
     conn_teardown();
 }
 
 #define FOUNDFDREPLY " ClamAV-Test-File.UNOFFICIAL FOUND"
 #define CLEANFDREPLY " OK"
 
0378a9ab
 static struct cmds {
     const char *cmd;
     const char term;
     const char *file;
     const char *reply;
 } fildes_cmds[] =
 {
     {"FILDES", '\n', SCANFILE, FOUNDFDREPLY},
     {"nFILDES", '\n', SCANFILE, FOUNDFDREPLY},
     {"zFILDES", '\0', SCANFILE, FOUNDFDREPLY},
     {"FILDES", '\n', CLEANFILE, CLEANFDREPLY},
     {"nFILDES", '\n', CLEANFILE, CLEANFDREPLY},
     {"zFILDES", '\0', CLEANFILE, CLEANFDREPLY}
 };
 
7bb82e6c
 #ifdef CHECK_HAVE_LOOPS
ec868782
 START_TEST (test_fildes)
 {
0378a9ab
     char nreply[BUFSIZ], nsend[BUFSIZ];
ec868782
     int fd = open(SCANFILE, O_RDONLY);
e9e0833f
     int closefd=0;
     int singlemsg=0;
0378a9ab
     const struct cmds *cmd;
     size_t nreply_len, nsend_len;
 
     switch (_i&3) {
 	case 0:
 	    closefd = 0;
 	    singlemsg = 0;
 	    break;
 	case 1:
 	    closefd = 1;
 	    singlemsg = 0;
 	    break;
 	case 2:
 	    closefd = 0;
 	    singlemsg = 1;
 	    break;
 	case 3:
 	    closefd = 1;
 	    singlemsg = 1;
 	    break;
     }
ec868782
 
0378a9ab
     cmd = &fildes_cmds[_i/4];
     nreply_len = snprintf(nreply, sizeof(nreply), "%s%c", cmd->reply, cmd->term);
     nsend_len = snprintf(nsend, sizeof(nsend), "%s%c", cmd->cmd, cmd->term);
ec868782
 
0378a9ab
     fd = open(cmd->file, O_RDONLY);
ec868782
     fail_unless_fmt(fd != -1, "Failed to open: %s\n", strerror(errno));
 
0378a9ab
     tst_fildes(nsend, nsend_len, fd, nreply, nreply_len, closefd, singlemsg);
ec868782
 
0378a9ab
     if (!closefd) {
 	/* closefd: 
 	 *  1 - close fd right after sending
 	 *  0 - close fd after receiving reply */
 	close(fd);
     }
 }
 END_TEST
7bb82e6c
 #endif
ec868782
 
0378a9ab
 START_TEST (test_fildes_many)
 {
     const char idsession[] = "zIDSESSION";
e9e0833f
     int dummyfd, i, killed = 0;
0378a9ab
     conn_setup();
     dummyfd = open(SCANFILE, O_RDONLY);
     fail_unless_fmt(dummyfd != -1, "failed to open %s: %s\n", SCANFILE, strerror(errno));
 
     fail_unless_fmt(send(sockd, idsession, sizeof(idsession), 0) == sizeof(idsession), "send IDSESSION failed\n");
73ba72eb
     for (i=0;i<1024;i++) {
0378a9ab
 	if (sendmsg_fd(sockd, "zFILDES", sizeof("zFILDES"), dummyfd, 1) == -1) {
 	    killed = 1;
 	    break;
 	}
     }
     close(dummyfd);
73ba72eb
     if (send(sockd, "zEND", sizeof("zEND"), 0) == -1) {
 	killed = 1;
     }
     conn_teardown();
 
     conn_setup();
     test_command("zPING", sizeof("zPING"), NULL, "PONG", 5);
0378a9ab
     conn_teardown();
ec868782
 }
 END_TEST
4e24a361
 
456e31a3
 START_TEST (test_fildes_unwanted)
 {
c5355aed
     char *recvdata;
     size_t len;
     int dummyfd;
     conn_setup();
     dummyfd = open(SCANFILE, O_RDONLY);
 
     /* send a 'zVERSION\0' including the ancillary data.
      * The \0 is from the extra char needed when sending ancillary data */
     fail_unless_fmt(sendmsg_fd(sockd, "zIDSESSION", strlen("zIDSESSION"), dummyfd, 1) != -1,
 		    "sendmsg failed: %s\n", strerror(errno));
 
     recvdata = recvfull(sockd, &len);
 
     fail_unless_fmt(!strcmp(recvdata,"1: PROTOCOL ERROR: ancillary data sent without FILDES. ERROR"),
 		    "Wrong reply: %s\n", recvdata);
 
     free(recvdata);
     close(dummyfd);
     conn_teardown();
 }
 END_TEST
 
 START_TEST (test_idsession_stress)
 {
456e31a3
     char buf[BUFSIZ];
     size_t i;
     char *data, *p;
     size_t len;
 
     conn_setup();
 
     fail_unless_fmt(send(sockd, "zIDSESSION", sizeof("zIDSESSION"), 0) == sizeof("zIDSESSION"),
 		    "send() failed: %s\n", strerror(errno));
     for (i=0;i < 1024; i++) {
b337768f
 	snprintf(buf, sizeof(buf), "%u", (unsigned)(i+1));
c5355aed
 	fail_unless(send(sockd, "zVERSION", sizeof("zVERSION"), 0) == sizeof("zVERSION"),
 		    "send failed: %s\n",strerror(errno));
456e31a3
 	data = recvpartial(sockd, &len, 1);
 	p = strchr(data, ':');
 	fail_unless_fmt(!!p, "wrong VERSION reply (%u): %s\n", i, data);
 	*p++ = '\0';
 	fail_unless_fmt(*p == ' ', "wrong VERSION reply (%u): %s\n", i, p);
 	*p++  = '\0';
 
 	fail_unless_fmt(!strcmp(p, VERSION_REPLY), "wrong VERSION reply: %s\n", data);
 	fail_unless_fmt(!strcmp(data, buf), "wrong IDSESSION id: %s\n", data);
 
 	free(data);
     }
 
     conn_teardown();
 }
 END_TEST
 
5f6edb22
 #define TIMEOUT_REPLY "TIMED OUT WAITING FOR COMMAND\n"
 
 START_TEST (test_connections)
 {
     int rc;
e9e0833f
     int i;
5f6edb22
     struct rlimit rlim;
     int *sock;
     int nf, maxfd=0;
     fail_unless_fmt(getrlimit(RLIMIT_NOFILE, &rlim) != -1,
 		    "Failed to get RLIMIT_NOFILE: %s\n", strerror(errno));
     nf = rlim.rlim_cur - 5;
     sock = malloc(sizeof(int)*nf);
 
     fail_unless(!!sock, "malloc failed\n");
 
     for (i=0;i<nf;i++) {
 	/* just open connections, and let them time out */
73ba72eb
 	conn_setup_mayfail(1);
 	if (sockd == -1) {
 	    nf = i;
 	    break;
 	}
5f6edb22
 	sock[i] = sockd;
 	if (sockd > maxfd)
 	    maxfd = sockd;
     }
     rc = fork();
     fail_unless(rc != -1, "fork() failed: %s\n", strerror(errno));
     if (rc == 0) {
 	char dummy;
 	int ret;
 	fd_set rfds;
 	FD_ZERO(&rfds);
 	for (i=0;i<nf;i++) {
 	    FD_SET(sock[i], &rfds);
 	}
 	while (1) {
 	    ret = select(maxfd+1, &rfds, NULL, NULL, NULL);
 	    if (ret < 0)
 		break;
 	    for (i=0;i<nf;i++) {
 		if (FD_ISSET(sock[i], &rfds)) {
 		    if (recv(sock[i], &dummy, 1, 0) == 0) {
 			close(sock[i]);
 			FD_CLR(sock[i], &rfds);
 		    }
 		}
 	    }
 	}
 	free(sock);
 	exit(0);
     } else {
 	for (i=0;i<nf;i++) {
 	    close(sock[i]);
 	}
 	free(sock);
 	/* now see if clamd is able to do anything else */
 	for (i=0;i<10;i++) {
 	    conn_setup();
 	    test_command("RELOAD", sizeof("RELOAD")-1, NULL, "RELOADING\n", sizeof("RELOADING\n")-1);
 	    conn_teardown();
 	}
     }
 }
 END_TEST
 
e114b109
 START_TEST (test_stream)
 {
     char buf[BUFSIZ];
     char *recvdata;
     size_t len;
     unsigned port;
     int streamsd, infd, nread;
 
     infd = open(SCANFILE, O_RDONLY);
 
     fail_unless_fmt(infd != -1, "open failed: %s\n", strerror(errno));
     conn_setup();
     fail_unless_fmt(
 	send(sockd, "zSTREAM", sizeof("zSTREAM"), 0) == sizeof("zSTREAM"),
 	"send failed: %s\n", strerror(errno));
     recvdata = recvpartial(sockd, &len, 1);
     fail_unless_fmt (sscanf(recvdata, "PORT %u\n", &port) == 1,
 		     "Wrong stream reply: %s\n", recvdata);
 
     free(recvdata);
     streamsd = conn_tcp(port);
 
     do {
 	nread = read(infd, buf, sizeof(buf));
 	if (nread > 0)
 	    fail_unless_fmt(send(streamsd, buf, nread, 0) == nread,
 			    "send failed: %s\n", strerror(errno));
     } while (nread > 0 || (nread == -1 && errno == EINTR));
     fail_unless_fmt(nread != -1, "read failed: %s\n", strerror(errno));
     close(infd);
     close(streamsd);
 
     recvdata = recvfull(sockd, &len);
     fail_unless_fmt(!strcmp(recvdata,"stream: ClamAV-Test-File.UNOFFICIAL FOUND"),
 		    "Wrong reply: %s\n", recvdata);
     free(recvdata);
 
     conn_teardown();
 }
 END_TEST
 
47d403f8
 #define END_CMD "zEND"
 #define INSTREAM_CMD "zINSTREAM"
 static void test_idsession_commands(int split, int instream)
 {
     char buf[20480];
     size_t i, len=0, j=0;
     char *recvdata;
     char *p = buf;
     const char *replies[2 + sizeof(basic_tests)/sizeof(basic_tests[0])];
 
     /* test all commands that must be accepted inside an IDSESSION */
     for (i=0;i < sizeof(basic_tests)/sizeof(basic_tests[0]); i++) {
 	const struct basic_test *test = &basic_tests[i];
92740b73
 	if (test->skiproot && isroot)
 	    continue;
47d403f8
 	if (test->ids == IDS_OK) {
 	    fail_unless(p+strlen(test->command)+2 < buf+sizeof(buf), "Buffer too small");
 	    *p++ = 'z';
 	    strcpy(p, test->command);
 	    p += strlen(test->command);
 	    *p++ = '\0';
 	    if (test->extra) {
 		fail_unless(p+strlen(test->extra) < buf+sizeof(buf), "Buffer too small");
 		strcpy(p, test->extra);
 		p += strlen(test->extra);
 	    }
 	    replies[j++] = test->reply;
 	}
 	if (instream && test->ids == IDS_END) {
 	    uint32_t chunk;
 	    /* IDS_END - in middle of other commands, perfect for inserting
 	     * INSTREAM */
 	    fail_unless(p+sizeof(INSTREAM_CMD)+544< buf+sizeof(buf), "Buffer too small");
 	    memcpy(p, INSTREAM_CMD, sizeof(INSTREAM_CMD));
 	    p += sizeof(INSTREAM_CMD);
 	    p += prepare_instream(p, 0, 552);
 	    replies[j++] = EXPECT_INSTREAM0;
 	    fail_unless(p+sizeof(INSTREAM_CMD)+16388< buf+sizeof(buf), "Buffer too small");
 	    memcpy(p, INSTREAM_CMD, sizeof(INSTREAM_CMD));
 	    p += sizeof(INSTREAM_CMD);
 	    chunk=htonl(16384);
 	    memcpy(p, &chunk, 4);
 	    p+=4;
 	    memset(p, 0x5a, 16384);
 	    p += 16384;
 	    *p++='\0';
 	    *p++='\0';
 	    *p++='\0';
 	    *p++='\0';
 	    replies[j++] = "stream: OK";
 	}
     }
     fail_unless(p+sizeof(END_CMD) < buf+sizeof(buf), "Buffer too small");
     memcpy(p, END_CMD, sizeof(END_CMD));
     p += sizeof(END_CMD);
 
     if (split) {
 	/* test corner-cases: 1-byte sends */
e9e0833f
 	for (i=0;i<(size_t)(p-buf);i++)
47d403f8
 	    fail_unless((size_t)send(sockd, &buf[i], 1, 0) == 1, "send() failed: %u, %s\n", i, strerror(errno));
     } else {
e9e0833f
 	fail_unless(send(sockd, buf, p-buf, 0) == p-buf,"send() failed: %s\n", strerror(errno));
47d403f8
     }
     recvdata = recvfull(sockd, &len);
     p = recvdata;
     for (i=0;i < sizeof(basic_tests)/sizeof(basic_tests[0]); i++) {
 	const struct basic_test *test = &basic_tests[i];
92740b73
 	if (test->skiproot && isroot)
 	    continue;
47d403f8
 	if (test->ids == IDS_OK) {
 	    unsigned id;
 	    char *q = strchr(p, ':');
 	    fail_unless_fmt(!!q, "No ID in reply: %s\n", p);
 	    *q = '\0';
 	    fail_unless_fmt(sscanf(p, "%u", &id) == 1,"Wrong ID in reply: %s\n", p);
 	    fail_unless(id > 0, "ID cannot be zero");
 	    fail_unless_fmt(id <= j, "ID too big: %u, max: %u\n", id, j);
 	    q += 2;
 	    fail_unless_fmt(!strcmp(q, replies[id-1]),
 			    "Wrong ID reply for ID %u: %s, expected %s\n",
 			    id,
 			    q, replies[id-1]);
 	    p = q + strlen(q)+1;
 	}
     }
     free(recvdata);
     conn_teardown();
 }
 
 #define ID_CMD "zIDSESSION"
 START_TEST(test_idsession)
 {
     conn_setup();
     fail_unless_fmt((size_t)send(sockd, ID_CMD, sizeof(ID_CMD), 0) == sizeof(ID_CMD),
 		    "send() failed: %s\n", strerror(errno));
     test_idsession_commands(0, 0);
     conn_setup();
     fail_unless_fmt((size_t)send(sockd, ID_CMD, sizeof(ID_CMD), 0) == sizeof(ID_CMD),
 		    "send() failed: %s\n", strerror(errno));
     test_idsession_commands(1, 0);
     conn_setup();
     fail_unless_fmt((size_t)send(sockd, ID_CMD, sizeof(ID_CMD), 0) == sizeof(ID_CMD),
 		    "send() failed: %s\n", strerror(errno));
     test_idsession_commands(0, 1);
 }
 END_TEST
 
05a213b8
 static Suite *test_clamd_suite(void)
 {
     Suite *s = suite_create("clamd");
0378a9ab
     TCase *tc_commands, *tc_stress;
     tc_commands = tcase_create("clamd commands");
05a213b8
     suite_add_tcase(s, tc_commands);
4e24a361
     tcase_add_unchecked_fixture(tc_commands, commands_setup, commands_teardown);
7bb82e6c
 #ifdef CHECK_HAVE_LOOPS
05a213b8
     tcase_add_loop_test(tc_commands, test_basic_commands, 0, sizeof(basic_tests)/sizeof(basic_tests[0]));
4e24a361
     tcase_add_loop_test(tc_commands, test_compat_commands, 0, sizeof(basic_tests)/sizeof(basic_tests[0]));
0378a9ab
     tcase_add_loop_test(tc_commands, test_fildes, 0, 4*sizeof(fildes_cmds)/sizeof(fildes_cmds[0]));
7bb82e6c
 #endif
ec868782
     tcase_add_test(tc_commands, test_stats);
     tcase_add_test(tc_commands, test_instream);
e114b109
     tcase_add_test(tc_commands, test_stream);
47d403f8
     tcase_add_test(tc_commands, test_idsession);
0378a9ab
     tc_stress = tcase_create("clamd stress test");
     suite_add_tcase(s, tc_stress);
73ba72eb
     tcase_set_timeout(tc_stress, 20);
0378a9ab
     tcase_add_test(tc_stress, test_fildes_many);
c5355aed
     tcase_add_test(tc_stress, test_idsession_stress);
     tcase_add_test(tc_stress, test_fildes_unwanted);
9e81ab6f
 #ifndef C_BSD
     /* FreeBSD and Darwin: connect() says connection refused on both
      * tcp/unix sockets, if I too quickly connect ~193 times, even if
      * listen backlog is higher.
770701eb
      * Don't run this test on BSD for now */
     tcase_add_test(tc_stress, test_connections);
9e81ab6f
 #endif
05a213b8
     return s;
 }
 
 int main(void)
 {
     int nf;
     Suite *s = test_clamd_suite();
     SRunner *sr = srunner_create(s);
73ba72eb
     srunner_set_log(sr, BUILDDIR"/test-clamd.log");
05a213b8
     srunner_run_all(sr, CK_NORMAL);
     nf = srunner_ntests_failed(sr);
     srunner_free(sr);
     return (nf == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
 }
 
 #else
 int main(void)
 {
     puts("\n*** Check version too old, clamd tests not run!\n");
     /* tell automake the test was skipped */
     return 77;
 }
 #endif