mirror of
https://github.com/Cisco-Talos/clamav.git
synced 2025-10-19 18:33:16 +00:00

The `realpath()` function on macOS will fail when scanning a symlink that doesn't exist because it tries to determine the real path of the thing the symlink points to and fails when that thing doesn't exist. This behavior is different than on Linux or FreeBSD Unix. I'm fixing this by opening the symlink with `O_SYMLINK` then getting realpath of the link using `cli_get_filepath_from_filedesc()`, much like we do on Windows. Side note: I did try using macOS's _DARWIN_BETTER_REALPATH variant to see if that resolved the issue, but it didn't seem to. This resolves https://bugzilla.clamav.net/show_bug.cgi?id=12792 This commit also removes the problematic "access denied" clamd test from the test suite. This commit broke that test on macOS because the error message when clamdscan fails to `open()` the file to check the realpath() is different than the error message when clamd tries to scan it, but has access denied. (It's like "Permission denied" instead of "Access denied") It's not worth #ifdef'ing around macOS to get a test pass because this test is already causing problems in 32-bit Docker environments where access isn't actually denied to the user (!). Honestly, it's not worth keeping the test that simply verifies ClamD's error message when denied matches expectations. I also switched to use the C_DARWIN macro instead of __APPLE__ because I kno C_DARWIN is defined in clamav-config.h on macOS, and I found that __APPLE__ wasn't defined when I tested.
902 lines
28 KiB
C
902 lines
28 KiB
C
/*
|
|
* Unit tests for clamd.
|
|
*
|
|
* Copyright (C) 2013-2021 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"
|
|
|
|
// common
|
|
#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, 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: %s\n", strerror(errno), SOCKET);
|
|
|
|
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 OBJDIR PATHSEP "input" PATHSEP "clamav_hdb_scanfiles" PATHSEP "clam.exe"
|
|
#define FOUNDREPLY "clam.exe: ClamAV-Test-File.UNOFFICIAL FOUND"
|
|
|
|
/* some clean file */
|
|
#define CLEANFILE SRCDIR PATHSEP "CMakeLists.txt"
|
|
#define CLEANREPLY CLEANFILE ": OK"
|
|
#define UNKNOWN_REPLY "UNKNOWN COMMAND"
|
|
|
|
#define NONEXISTENT PATHSEP "nonexistentfilename"
|
|
|
|
#define NONEXISTENT_REPLY NONEXISTENT ": File path check failure: No such file or directory. ERROR"
|
|
|
|
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 | O_BINARY);
|
|
if (fd != -1) close(fd);
|
|
ck_assert_msg(fd == -1, "Nonexistent file exists!\n");
|
|
|
|
#ifndef _WIN32
|
|
/* Prepare the "isroot" global so we can skip some tests when run as root */
|
|
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 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)
|
|
{
|
|
char *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 = (char *)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.\nReceived: \n%s\nExpected: \n%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 = (char *)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 != " STDi64 ", %s\n", nread, (int64_t)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)
|
|
{
|
|
char *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 = (char *)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:\nReceived: \n%s\nExpected: \n%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 = (char *)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:\nReceived: \n%s\nExpected: \n%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";
|
|
const char fildes[] = "zFILDES";
|
|
const char end[] = "zEND";
|
|
const char ping[] = "zPING";
|
|
|
|
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, fildes, sizeof(fildes), dummyfd, 1) == -1) {
|
|
killed = 1;
|
|
break;
|
|
}
|
|
}
|
|
close(dummyfd);
|
|
if (send(sockd, end, sizeof(end), 0) == -1) {
|
|
killed = 1;
|
|
}
|
|
conn_teardown();
|
|
|
|
conn_setup();
|
|
test_command(ping, sizeof(ping), NULL, "PONG", 5);
|
|
conn_teardown();
|
|
}
|
|
END_TEST
|
|
|
|
START_TEST(test_fildes_unwanted)
|
|
{
|
|
char *recvdata;
|
|
size_t len;
|
|
int dummyfd;
|
|
const char idsession[] = "zIDSESSION";
|
|
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, idsession, sizeof(idsession), dummyfd, 1) != -1,
|
|
"sendmsg failed: %s\n", strerror(errno));
|
|
|
|
recvdata = (char *)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;
|
|
const char idsession[] = "zIDSESSION";
|
|
const char version[] = "zVERSION";
|
|
|
|
conn_setup();
|
|
|
|
ck_assert_msg(send(sockd, idsession, sizeof(idsession), 0) == sizeof(idsession),
|
|
"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, version, sizeof(version), 0) == sizeof(version),
|
|
"send failed: %s\n", strerror(errno));
|
|
data = recvpartial(sockd, &len, 1);
|
|
p = strchr(data, ':');
|
|
ck_assert_msg(!!p, "wrong VERSION reply (%zu): %s\n", i, data);
|
|
*p++ = '\0';
|
|
ck_assert_msg(*p == ' ', "wrong VERSION reply (%zu): %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 = MIN(rlim.rlim_cur - 5, 250);
|
|
|
|
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: %zu, %s\n", i, strerror(errno));
|
|
} else {
|
|
ck_assert_msg(send(sockd, buf, p - buf, 0) == p - buf, "send() failed: %s\n", strerror(errno));
|
|
}
|
|
recvdata = (char *)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: %zu\n", id, j);
|
|
q += 2;
|
|
ck_assert_msg(NULL != strstr(q, replies[id - 1]),
|
|
"Wrong ID reply for ID %u:\nReceived: \n%s\nExpected: \n%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, OBJDIR 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;
|
|
}
|