clamav/unit_tests/check_clamd.c
Micah Snyder c81968d3a7 GitHub Actions testing on Ubuntu, Mac, & Windows
Updates to fix issues in the CMake install instructions.

Updates the README.md to indicate that CMake is now preferred

Adds a GitHub Actions badge, Discord badge, and logo to the README.md.

CMake:

- Renamed ENABLE_DOCS to ENABLE_MAN_PAGES.

- Fixed build issue when milter isn't enabled on Linux. Changed the
default to build milter on non-macOS, non-Windows operating systems.

- Fix LD_LIBRARY_PATH for tests including on macOS where LD_LIBRARY_PATH
  and DYLD_LIBRARY_PATH must be manually propagated to subprocesses.

- Use UNKNOWN IMPORTED library instead of INTERFACE IMPORTED library for
  pdcurses, but still use INTERFACE IMPORTED for ncurses.
  UNKNOWN IMPORTED appears to be required so that we can use
  $<TARGET_FILE_DIR:Curses::curses> to collected the pdcurses library at
  install time on Windows.

- When building with vcpkg on Windows, CMake will automatically install
  your app local dependencies (aka the DLL runtime dependencies).
  Meanwhile, file(GET_RUNTIME_DEPENDENCIES ...) doesn't appear to work
  correctly with vcpkg packages. The solution is to use a custom target
  that has CMake perform a local install to the unit_tests directory
  when using vcpkg.
  This is in fact far easier than using GET_RUNTIME_DEPENDENCIES in the
  unit_tests for assembling the test environment but we can't use this
  method for the non-vcpkg install because it won't collect
  checkDynamic.dll for us because we don't install our tests.
  We also can't link with the static check.lib because the static
  check.lib has pthreads symbols linked in and will conflict with our
  pthread.dll.

  TL;DR: We'll continue to use file(GET_RUNTIME_DEPENDENCIES ...) for
  assembling the test enviornment on non-vcpkg builds, and use the local
  install method for vcpkg builds.

testcase.py: Wrapped a Pathlib.unlink() call in exception handling as
the missing_ok optional parameter requires a Python version too new for
common use.

Remove localtime_r from win32 compat lib.
localtime_r may be present in libcheck when building with vcpkg and
while making it a static function would also solve the issue, using
localtime_s instead like we do everywhere else should work just fine.

check_clamd: Limited the max # of connections for the stress test on Mac
to 850, to address issues found testing on macos-latest on GitHub Actions.
2021-02-25 11:41:28 -08:00

930 lines
29 KiB
C

/*
* Unit tests for clamd.
*
* Copyright (C) 2013-2020 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
* Copyright (C) 2009-2013 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.
*/
#if HAVE_CONFIG_H
#include "clamav-config.h"
#endif
#ifndef _WIN32
#include <arpa/inet.h>
#include <netinet/in.h>
#endif
#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
#ifdef _WIN32
#include <windows.h>
#include <winsock2.h>
#else
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#endif
#include <sys/stat.h>
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#include <check.h>
// libclamav
#include "clamav.h"
#include "platform.h"
#include "version.h"
#include "str.h"
// shared
#include "fdpassing.h"
static int conn_tcp(int port)
{
struct sockaddr_in server;
int rc;
int sd = socket(AF_INET, SOCK_STREAM, 0);
ck_assert_msg(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, (socklen_t)sizeof(server));
ck_assert_msg(rc != -1, "Unable to connect(): %s\n", strerror(errno));
return sd;
}
static int sockd;
#ifndef _WIN32
#define SOCKET "clamd-test.socket"
static void conn_setup_mayfail(int may)
{
int rc;
struct sockaddr_un nixsock;
memset((void *)&nixsock, 0, sizeof(nixsock));
nixsock.sun_family = AF_UNIX;
strncpy(nixsock.sun_path, BUILDDIR PATHSEP SOCKET, sizeof(nixsock.sun_path));
sockd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sockd == -1 && (may && (errno == EMFILE || errno == ENFILE)))
return;
ck_assert_msg(sockd != -1, "Unable to create socket: %s\n", strerror(errno));
rc = connect(sockd, (struct sockaddr *)&nixsock, (socklen_t)sizeof(nixsock));
if (rc == -1 && (may && (errno == ECONNREFUSED))) {
close(sockd);
sockd = -1;
return;
}
ck_assert_msg(rc != -1, "Unable to connect(): %s\n", strerror(errno));
signal(SIGPIPE, SIG_IGN);
}
#else
#define PORT 3319
static void conn_setup_mayfail(int may)
{
sockd = conn_tcp(PORT);
if (sockd == -1 && (may && (errno == ECONNREFUSED)))
return;
ck_assert_msg(sockd != -1, "Unable to connect(): %s\n", strerror(errno));
}
#endif
static void conn_setup(void)
{
conn_setup_mayfail(0);
}
static void conn_teardown(void)
{
if (sockd != -1)
#ifndef _WIN32
close(sockd);
#else
closesocket(sockd);
#endif
}
#ifndef REPO_VERSION
#define REPO_VERSION VERSION
#endif
#define SCANFILE BUILDDIR PATHSEP ".." PATHSEP "test" PATHSEP "clam.exe"
#define FOUNDREPLY "clam.exe: ClamAV-Test-File.UNOFFICIAL FOUND"
/* some clean file */
#define CLEANFILE SRCDIR PATHSEP "Makefile.am"
#define CLEANREPLY CLEANFILE ": OK"
#define UNKNOWN_REPLY "UNKNOWN COMMAND"
#define NONEXISTENT PATHSEP "nonexistent\vfilename"
#define NONEXISTENT_REPLY NONEXISTENT ": lstat() failed: No such file or directory. ERROR"
#ifndef _WIN32
#define ACCDENIED BUILDDIR PATHSEP "accdenied"
#define ACCDENIED_REPLY ACCDENIED ": Access denied. ERROR"
#endif
static int isroot = 0;
static void commands_setup(void)
{
#ifndef _WIN32
const char *nonempty = "NONEMPTYFILE";
#endif
/*
* Verify that our NONEXISTENT filepath indeed does not exist.
*/
int fd = open(NONEXISTENT, O_RDONLY);
if (fd != -1) close(fd);
ck_assert_msg(fd == -1, "Nonexistent file exists!\n");
#ifndef _WIN32
/*
* Prepare a file path that is write-only.
* Note: doesn't work on Windows (O_RWONLY is implicitly readable), so we skip this test on Windows.
*/
fd = open(ACCDENIED, O_CREAT | O_WRONLY, S_IWUSR);
ck_assert_msg(fd != -1,
"Failed to create file for access denied tests: %s\n", strerror(errno));
ck_assert_msg(fchmod(fd, S_IWUSR) != -1,
"Failed to chmod: %s\n", strerror(errno));
/* must not be empty file */
ck_assert_msg((size_t)write(fd, nonempty, strlen(nonempty)) == strlen(nonempty),
"Failed to write into testfile: %s\n", strerror(errno));
close(fd);
/* Prepare the "isroot" global so we can skip the access-denied tests when run as root
because... you know, root will ignore permissions and still read the file. */
if (!geteuid()) {
isroot = 1;
}
#endif
}
static void commands_teardown(void)
{
}
#define VERSION_REPLY "ClamAV " REPO_VERSION "" VERSION_SUFFIX
#define VCMDS_REPLY VERSION_REPLY "| COMMANDS: SCAN QUIT RELOAD PING CONTSCAN VERSIONCOMMANDS VERSION END SHUTDOWN MULTISCAN FILDES STATS IDSESSION INSTREAM DETSTATSCLEAR DETSTATS ALLMATCHSCAN"
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 */
};
static struct basic_test {
const char *command;
const char *extra;
const char *reply;
int support_old;
int skiproot;
enum idsession_support ids;
} basic_tests[] = {
{"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},
/* unknown commands */
{"RANDOM", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
/* commands invalid as first */
{"END", NULL, UNKNOWN_REPLY, 1, 0, IDS_END},
/* commands for nonexistent files */
{"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},
/* commands for access denied files */
#ifndef _WIN32
{"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},
#endif
/* commands with invalid/missing arguments */
{"SCAN", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
{"CONTSCAN", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
{"MULTISCAN", NULL, UNKNOWN_REPLY, 1, 0, IDS_REJECT},
/* commands with invalid data */
{"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 */
};
static void *recvpartial(int sd, size_t *len, int partial)
{
char *buf = NULL;
size_t off = 0;
int rc;
*len = 0;
do {
if (off + BUFSIZ > *len) {
*len += BUFSIZ + 1;
buf = realloc(buf, *len);
ck_assert_msg(!!buf, "Cannot realloc buffer\n");
}
rc = recv(sd, buf + off, BUFSIZ, 0);
ck_assert_msg(rc != -1, "recv() failed: %s\n", strerror(errno));
off += rc;
} while (rc && (!partial || !memchr(buf, '\0', off)));
*len = off;
buf[*len] = '\0';
return buf;
}
static void *recvfull(int sd, size_t *len)
{
return recvpartial(sd, len, 0);
}
static void test_command(const char *cmd, size_t len, const char *extra, const char *expect, size_t expect_len)
{
void *recvdata;
ssize_t rc;
char *expected_string_offset = NULL;
rc = send(sockd, cmd, len, 0);
ck_assert_msg((size_t)rc == len, "Unable to send(): %s\n", strerror(errno));
if (extra) {
rc = send(sockd, extra, strlen(extra), 0);
ck_assert_msg((size_t)rc == strlen(extra), "Unable to send() extra for %s: %s\n", cmd, strerror(errno));
}
#ifdef _WIN32
shutdown(sockd, SD_SEND);
#else
shutdown(sockd, SHUT_WR);
#endif
recvdata = recvfull(sockd, &len);
// The path which comes back may be an absolute real path, not a relative path with symlinks ...
// ... so this length check isn't really meaningful anymore.
// For the same reasons, we can't expect the path to match exactly, so we'll
// just make sure expect is found in recvdata and use the basename instead of the full path.
expected_string_offset = CLI_STRNSTR(recvdata, expect, len);
ck_assert_msg(expected_string_offset != NULL, "Wrong reply for command %s: |%s|, expected: |%s|\n", cmd, recvdata, expect);
free(recvdata);
}
START_TEST(test_basic_commands)
{
struct basic_test *test = &basic_tests[_i];
char nsend[BUFSIZ], nreply[BUFSIZ];
if (test->skiproot && isroot)
return;
/* send nCOMMAND */
snprintf(nreply, sizeof(nreply), "%s\n", test->reply);
snprintf(nsend, sizeof(nsend), "n%s\n", test->command);
conn_setup();
test_command(nsend, strlen(nsend), test->extra, nreply, strlen(nreply));
conn_teardown();
/* send zCOMMAND */
snprintf(nsend, sizeof(nsend), "z%s", test->command);
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];
if (test->skiproot && isroot)
return;
if (!test->support_old) {
snprintf(nreply, sizeof(nreply), "UNKNOWN COMMAND\n");
test->extra = NULL;
} else {
snprintf(nreply, sizeof(nreply), "%s\n", test->reply);
}
/* one command = one packet, no delimiter */
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();
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();
}
}
END_TEST
#define EXPECT_INSTREAM "stream: ClamAV-Test-File.UNOFFICIAL FOUND\n"
#define EXPECT_INSTREAM0 "stream: ClamAV-Test-File.UNOFFICIAL FOUND"
#define STATS_REPLY "POOLS: 1\n\nSTATE: VALID PRIMARY\n"
START_TEST(test_stats)
{
char *recvdata;
size_t len = strlen("nSTATS\n");
int rc;
conn_setup();
rc = send(sockd, "nSTATS\n", len, 0);
ck_assert_msg((size_t)rc == len, "Unable to send(): %s\n", strerror(errno));
recvdata = recvfull(sockd, &len);
ck_assert_msg(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);
ck_assert_msg(rc == 0, "Wrong reply: %s\n", recvdata);
free(recvdata);
conn_teardown();
}
END_TEST
static size_t prepare_instream(char *buf, size_t off, size_t buflen)
{
STATBUF stbuf;
int fd, nread;
uint32_t chunk;
ck_assert_msg(CLAMSTAT(SCANFILE, &stbuf) != -1, "stat failed for %s: %s", SCANFILE, strerror(errno));
fd = open(SCANFILE, O_RDONLY | O_BINARY);
ck_assert_msg(fd != -1, "open failed: %s\n", strerror(errno));
chunk = htonl(stbuf.st_size);
memcpy(&buf[off], &chunk, sizeof(chunk));
off += 4;
nread = read(fd, &buf[off], buflen - off - 4);
ck_assert_msg(nread == stbuf.st_size, "read failed: %d != %d, %s\n", nread, stbuf.st_size, strerror(errno));
off += nread;
buf[off++] = 0;
buf[off++] = 0;
buf[off++] = 0;
buf[off++] = 0;
close(fd);
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));
conn_setup();
ck_assert_msg((size_t)send(sockd, buf, off, 0) == off, "send() failed: %s\n", strerror(errno));
recvdata = recvfull(sockd, &len);
expect_len = strlen(EXPECT_INSTREAM);
ck_assert_msg(len == expect_len, "Reply has wrong size: %lu, expected %lu, reply: %s\n",
len, expect_len, recvdata);
rc = memcmp(recvdata, EXPECT_INSTREAM, expect_len);
ck_assert_msg(!rc, "Wrong reply for command INSTREAM: |%s|, expected: |%s|\n", recvdata, EXPECT_INSTREAM);
free(recvdata);
conn_teardown();
}
END_TEST
#ifndef _WIN32
static int sendmsg_fd(int sockd, const char *mesg, size_t msg_len, int fd, int singlemsg)
{
struct msghdr msg;
struct cmsghdr *cmsg;
unsigned char fdbuf[CMSG_SPACE(sizeof(int))];
char dummy[BUFSIZ];
struct iovec iov[1];
int rc;
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 */
ck_assert_msg(msg_len < sizeof(dummy) - 1, "message too large");
memcpy(dummy, mesg, msg_len);
dummy[msg_len] = '\0';
iov[0].iov_base = dummy;
iov[0].iov_len = msg_len + 1;
}
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;
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;
conn_setup();
ck_assert_msg(sendmsg_fd(sockd, cmd, len, fd, singlemsg) != -1,
"Failed to sendmsg: %s\n", strerror(errno));
if (closefd)
close(fd);
recvdata = recvfull(sockd, &len);
p = strchr(recvdata, ':');
ck_assert_msg(!!p, "Reply doesn't contain ':' : %s\n", recvdata);
*p++ = '\0';
ck_assert_msg(sscanf(recvdata, "fd[%u]", &rc) == 1, "Reply doesn't contain fd: %s\n", recvdata);
len -= p - recvdata;
ck_assert_msg(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);
ck_assert_msg(!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"
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}};
START_TEST(test_fildes)
{
char nreply[BUFSIZ], nsend[BUFSIZ];
int fd = open(SCANFILE, O_RDONLY);
int closefd = 0;
int singlemsg = 0;
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;
}
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);
fd = open(cmd->file, O_RDONLY);
ck_assert_msg(fd != -1, "Failed to open: %s\n", strerror(errno));
tst_fildes(nsend, nsend_len, fd, nreply, nreply_len, closefd, singlemsg);
if (!closefd) {
/* closefd:
* 1 - close fd right after sending
* 0 - close fd after receiving reply */
close(fd);
}
}
END_TEST
START_TEST(test_fildes_many)
{
const char idsession[] = "zIDSESSION";
int dummyfd, i, killed = 0;
conn_setup();
dummyfd = open(SCANFILE, O_RDONLY);
ck_assert_msg(dummyfd != -1, "failed to open %s: %s\n", SCANFILE, strerror(errno));
ck_assert_msg(send(sockd, idsession, sizeof(idsession), 0) == sizeof(idsession), "send IDSESSION failed\n");
for (i = 0; i < 1024; i++) {
if (sendmsg_fd(sockd, "zFILDES", sizeof("zFILDES"), dummyfd, 1) == -1) {
killed = 1;
break;
}
}
close(dummyfd);
if (send(sockd, "zEND", sizeof("zEND"), 0) == -1) {
killed = 1;
}
conn_teardown();
conn_setup();
test_command("zPING", sizeof("zPING"), NULL, "PONG", 5);
conn_teardown();
}
END_TEST
START_TEST(test_fildes_unwanted)
{
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 */
ck_assert_msg(sendmsg_fd(sockd, "zIDSESSION", strlen("zIDSESSION"), dummyfd, 1) != -1,
"sendmsg failed: %s\n", strerror(errno));
recvdata = recvfull(sockd, &len);
ck_assert_msg(!strcmp(recvdata, "1: PROTOCOL ERROR: ancillary data sent without FILDES. ERROR"),
"Wrong reply: %s\n", recvdata);
free(recvdata);
close(dummyfd);
conn_teardown();
}
END_TEST
#endif
START_TEST(test_idsession_stress)
{
char buf[BUFSIZ];
size_t i;
char *data, *p;
size_t len;
conn_setup();
ck_assert_msg(send(sockd, "zIDSESSION", sizeof("zIDSESSION"), 0) == sizeof("zIDSESSION"),
"send() failed: %s\n", strerror(errno));
for (i = 0; i < 1024; i++) {
snprintf(buf, sizeof(buf), "%u", (unsigned)(i + 1));
ck_assert_msg(send(sockd, "zVERSION", sizeof("zVERSION"), 0) == sizeof("zVERSION"),
"send failed: %s\n", strerror(errno));
data = recvpartial(sockd, &len, 1);
p = strchr(data, ':');
ck_assert_msg(!!p, "wrong VERSION reply (%u): %s\n", i, data);
*p++ = '\0';
ck_assert_msg(*p == ' ', "wrong VERSION reply (%u): %s\n", i, p);
*p++ = '\0';
ck_assert_msg(!strcmp(p, VERSION_REPLY), "wrong VERSION reply: %s\n", data);
ck_assert_msg(!strcmp(data, buf), "wrong IDSESSION id: %s\n", data);
free(data);
}
conn_teardown();
}
END_TEST
#define TIMEOUT_REPLY "TIMED OUT WAITING FOR COMMAND\n"
#ifndef _WIN32
/*
* Test that we can still interact with clamd when it has a lot of active connections.
*
* Porting this test to work on Windows is too tedious at present.
* I suspect it should be rewritten using threads. For now, skip on Windows.
*/
START_TEST(test_connections)
{
int rc;
int i;
struct rlimit rlim;
int *sock;
int num_fds, maxfd = 0;
ck_assert_msg(getrlimit(RLIMIT_NOFILE, &rlim) != -1,
"Failed to get RLIMIT_NOFILE: %s\n", strerror(errno));
num_fds = rlim.rlim_cur - 5;
#ifdef C_DARWIN
/* While the limit when testing on macOS appears to be aroudn 1024, though
in testing on github actions macos-latest, get "connection refused"
after ~925 give or take.
It's possible the getrlimit API is incorrect post macOS Sierra, which
may explain the issue. In any case, limiting to 850 should be safe.
It's possible there' ssome other limitation at play on the GitHUb's
macos-latest shared environment. */
num_fds = MIN(num_fds, 850);
#endif
sock = malloc(sizeof(int) * num_fds);
ck_assert_msg(!!sock, "malloc failed\n");
for (i = 0; i < num_fds; i++) {
/* just open connections, and let them time out */
conn_setup_mayfail(1);
if (sockd == -1) {
/* close the previous one, to leave space for one more connection */
i--;
close(sock[i]);
sock[i] = -1;
num_fds = i;
break;
}
sock[i] = sockd;
if (sockd > maxfd)
maxfd = sockd;
}
rc = fork();
ck_assert_msg(rc != -1, "fork() failed: %s\n", strerror(errno));
if (rc == 0) {
/* Child */
char dummy;
int ret;
fd_set rfds;
FD_ZERO(&rfds);
for (i = 0; i < num_fds; i++) {
FD_SET(sock[i], &rfds);
}
while (1) {
ret = select(maxfd + 1, &rfds, NULL, NULL, NULL);
if (ret < 0)
break;
for (i = 0; i < num_fds; 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 {
/* Parent */
for (i = 0; i < num_fds; 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();
}
/* Ok we're done, kill the child process if it's still up, else it might hang the test framework */
kill(rc, SIGKILL);
}
}
END_TEST
#endif
#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];
if (test->skiproot && isroot)
continue;
if (test->ids == IDS_OK) {
ck_assert_msg(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) {
ck_assert_msg(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 */
ck_assert_msg(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;
ck_assert_msg(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";
}
}
ck_assert_msg(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 */
for (i = 0; i < (size_t)(p - buf); i++)
ck_assert_msg((size_t)send(sockd, &buf[i], 1, 0) == 1, "send() failed: %u, %s\n", i, strerror(errno));
} else {
ck_assert_msg(send(sockd, buf, p - buf, 0) == p - buf, "send() failed: %s\n", strerror(errno));
}
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];
if (test->skiproot && isroot)
continue;
if (test->ids == IDS_OK) {
unsigned id;
char *q = strchr(p, ':');
ck_assert_msg(!!q, "No ID in reply: %s\n", p);
*q = '\0';
ck_assert_msg(sscanf(p, "%u", &id) == 1, "Wrong ID in reply: %s\n", p);
ck_assert_msg(id > 0, "ID cannot be zero");
ck_assert_msg(id <= j, "ID too big: %u, max: %u\n", id, j);
q += 2;
ck_assert_msg(NULL != strstr(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();
ck_assert_msg((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();
ck_assert_msg((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();
ck_assert_msg((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
static Suite *test_clamd_suite(void)
{
Suite *s = suite_create("clamd");
TCase *tc_commands, *tc_stress;
tc_commands = tcase_create("clamd commands");
suite_add_tcase(s, tc_commands);
tcase_add_unchecked_fixture(tc_commands, commands_setup, commands_teardown);
tcase_add_loop_test(tc_commands, test_basic_commands, 0, sizeof(basic_tests) / sizeof(basic_tests[0]));
tcase_add_loop_test(tc_commands, test_compat_commands, 0, sizeof(basic_tests) / sizeof(basic_tests[0]));
#ifndef _WIN32 // Disabled on Windows because fd-passing not supported on Windows
tcase_add_loop_test(tc_commands, test_fildes, 0, 4 * sizeof(fildes_cmds) / sizeof(fildes_cmds[0]));
#endif
tcase_add_test(tc_commands, test_stats);
tcase_add_test(tc_commands, test_instream);
tcase_add_test(tc_commands, test_idsession);
#ifndef _WIN32 // Disabled because fd-passing not supported on Windows
tc_stress = tcase_create("clamd stress test");
suite_add_tcase(s, tc_stress);
tcase_set_timeout(tc_stress, 20);
tcase_add_test(tc_stress, test_fildes_many);
tcase_add_test(tc_stress, test_idsession_stress);
tcase_add_test(tc_stress, test_fildes_unwanted);
#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.
* Don't run this test on BSD for now */
tcase_add_test(tc_stress, test_connections); // Disabled on Windows because test uses fork() instead of threads, and needs to be rewritten.
#endif
#endif
return s;
}
int main(void)
{
int num_fds;
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != NO_ERROR) {
fprintf(stderr, "Error at WSAStartup(): %d\n", WSAGetLastError());
return EXIT_FAILURE;
}
#endif
Suite *s = test_clamd_suite();
SRunner *sr = srunner_create(s);
srunner_set_log(sr, BUILDDIR PATHSEP "test-clamd.log");
srunner_run_all(sr, CK_NORMAL);
num_fds = srunner_ntests_failed(sr);
srunner_free(sr);
return (num_fds == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}