mirror of
https://github.com/Cisco-Talos/clamav.git
synced 2025-10-19 10:23:17 +00:00

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.
930 lines
29 KiB
C
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;
|
|
}
|