Commit dfcf0387a for clamav.net
commit dfcf0387af878817db01a41b7b7d0cff4bd775b7
Author: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com>
Date: Thu Jan 29 21:27:43 2026 +0100
feat: add SELFCHECK command and related configuration options (#1551)
Add SELFCHECK command and related configuration options
Document SELFCHECK command in clamd.conf.sample.
Document EnableSelfCheckCommand in clamd.conf.
Implement robust SELFCHECK test with retries.
Added robust SELFCHECK test to handle RELOADING state.
diff --git a/clamd/server-th.c b/clamd/server-th.c
index f75235007..79c046bce 100644
--- a/clamd/server-th.c
+++ b/clamd/server-th.c
@@ -192,7 +192,7 @@ void sighandler_th(int sig)
logg(LOGG_DEBUG_NV, "Failed to write to syncpipe\n");
}
-static int need_db_reload(void)
+int need_db_reload(void)
{
if (!dbstat.entries) {
logg(LOGG_INFO, "No stats for Database check - forcing reload\n");
diff --git a/clamd/server.h b/clamd/server.h
index 6cefb5a73..c619813aa 100644
--- a/clamd/server.h
+++ b/clamd/server.h
@@ -35,6 +35,8 @@
#include "thrmgr.h"
#include "session.h"
+int need_db_reload(void);
+
struct thrarg {
int sid;
struct cl_scan_options *options;
diff --git a/clamd/session.c b/clamd/session.c
index e01359be5..617f14e19 100644
--- a/clamd/session.c
+++ b/clamd/session.c
@@ -83,6 +83,7 @@ static struct {
{CMD1, sizeof(CMD1) - 1, COMMAND_SCAN, 1, 1, 0},
{CMD3, sizeof(CMD3) - 1, COMMAND_SHUTDOWN, 0, 1, 0},
{CMD4, sizeof(CMD4) - 1, COMMAND_RELOAD, 0, 1, 0},
+ {CMD25, sizeof(CMD25) - 1, COMMAND_SELFCHECK, 0, 1, 0},
{CMD5, sizeof(CMD5) - 1, COMMAND_PING, 0, 1, 0},
{CMD6, sizeof(CMD6) - 1, COMMAND_CONTSCAN, 1, 1, 0},
/* must be before VERSION, because they share common prefix! */
@@ -570,6 +571,31 @@ int execute_or_dispatch_command(client_conn_t *conn, enum commands cmd, const ch
conn_reply_single(conn, NULL, "COMMAND UNAVAILABLE");
}
return 1;
+ case COMMAND_SELFCHECK:
+ if (optget(conn->opts, "EnableSelfCheckCommand")->enabled) {
+ int reload_flag;
+ int db_reload_needed;
+ pthread_mutex_lock(&reload_mutex);
+ reload_flag = reload;
+ pthread_mutex_unlock(&reload_mutex);
+ if (reload_flag) {
+ mdprintf(desc, "RELOADING%c", term);
+ return 1;
+ }
+ db_reload_needed = need_db_reload();
+ if (db_reload_needed) {
+ pthread_mutex_lock(&reload_mutex);
+ reload = 1;
+ pthread_mutex_unlock(&reload_mutex);
+ mdprintf(desc, "RELOADING%c", term);
+ return 1;
+ }
+ mdprintf(desc, "DBUPTODATE%c", term);
+ return 1;
+ } else {
+ conn_reply_single(conn, NULL, "COMMAND UNAVAILABLE");
+ return 1;
+ }
case COMMAND_PING:
if (conn->group)
mdprintf(desc, "%u: PONG%c", conn->id, term);
diff --git a/clamd/session.h b/clamd/session.h
index 4a8ffac1c..64566aa8d 100644
--- a/clamd/session.h
+++ b/clamd/session.h
@@ -49,6 +49,8 @@
#define CMD23 "GET / HTTP/2"
#define CMD24 ""
+#define CMD25 "SELFCHECK"
+
// libclamav
#include "clamav.h"
@@ -62,6 +64,7 @@ enum commands {
COMMAND_UNKNOWN = 0,
COMMAND_SHUTDOWN = 1,
COMMAND_RELOAD,
+ COMMAND_SELFCHECK,
COMMAND_END,
COMMAND_SCAN,
COMMAND_PING,
diff --git a/common/optparser.c b/common/optparser.c
index b3c415553..46f6fd191 100644
--- a/common/optparser.c
+++ b/common/optparser.c
@@ -322,6 +322,8 @@ const struct clam_option __clam_options[] = {
{"EnableReloadCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the RELOAD command for clamd", "no"},
+ {"EnableSelfCheckCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the SELFCHECK command for clamd", "no"},
+
{"EnableVersionCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the VERSION command for clamd", "yes"},
{"EnableStatsCommand", NULL, 0, CLOPT_TYPE_BOOL, MATCH_BOOL, 1, NULL, 0, OPT_CLAMD, "Enables the STATS command for clamd", "yes"},
diff --git a/docs/man/clamd.conf.5.in b/docs/man/clamd.conf.5.in
index a77501471..8e28ff96d 100644
--- a/docs/man/clamd.conf.5.in
+++ b/docs/man/clamd.conf.5.in
@@ -163,6 +163,13 @@ When disabled, clamd responds to this command with COMMAND UNAVAILABLE.
.br
Default: yes
.TP
+\EnableSelfCheckCommand BOOL\fR
+Enables the SELFCHECK command. Setting this to no prevents a client to reload the database.
+.br
+When disabled, clamd responds to this command with COMMAND UNAVAILABLE. When enabled and reload is required clamd responds RELOADING. If reload not needed - DBUPTODATE.
+.br
+Default: yes
+.TP
\fBEnableVersionCommand BOOL\fR
Enables the VERSION command. Setting this to no prevents a client from querying version information.
.br
diff --git a/etc/clamd.conf.sample b/etc/clamd.conf.sample
index 73be60eae..116822c4d 100644
--- a/etc/clamd.conf.sample
+++ b/etc/clamd.conf.sample
@@ -151,6 +151,11 @@ Example
# Default: yes
#EnableReloadCommand no
#
+# Enable the SELFCHECK command
+# Setting this to no prevents a client to reload the database.
+# Default: yes
+#EnableSelfCheckCommand no
+#
# Enable the STATS command
# Setting this to no prevents a client from querying statistics.
# Default: yes
diff --git a/unit_tests/check_clamd.c b/unit_tests/check_clamd.c
index 204ee27f3..5b9ddaee9 100644
--- a/unit_tests/check_clamd.c
+++ b/unit_tests/check_clamd.c
@@ -177,7 +177,7 @@ 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"
+#define VCMDS_REPLY VERSION_REPLY "| COMMANDS: SCAN QUIT RELOAD SELFCHECK PING CONTSCAN VERSIONCOMMANDS VERSION END SHUTDOWN MULTISCAN FILDES STATS IDSESSION INSTREAM DETSTATSCLEAR DETSTATS ALLMATCHSCAN"
enum idsession_support {
IDS_OK, /* accepted */
@@ -376,6 +376,64 @@ START_TEST(test_stats)
}
END_TEST
+/* Robust SELFCHECK: tolerate RELOADING for a short while, then require DBUPTODATE */
+#define SELFCHECK_EXPECT "DBUPTODATE"
+#define SELFCHECK_RELOADING "RELOADING"
+START_TEST(test_selfcheck)
+{
+ char *recvdata = NULL;
+ size_t len;
+ int rc;
+ int attempts = 0;
+ const int max_attempts = 60; /* timeout ~3m with check each 3s */
+ const int sleep_ms = 3000;
+
+ conn_setup();
+
+ do {
+ const char *cmd = "nSELFCHECK\n";
+ len = strlen(cmd);
+ rc = send(sockd, cmd, len, 0);
+ ck_assert_msg((size_t)rc == len, "Unable to send(): %s\n", strerror(errno));
+
+ recvdata = (char *)recvfull(sockd, &len);
+ ck_assert_msg(recvdata != NULL, "recvfull() returned NULL");
+
+ /* Trim trailing newlines */
+ while (len > 0 && (recvdata[len - 1] == '\n' || recvdata[len - 1] == '\r')) {
+ recvdata[--len] = '\0';
+ }
+
+ if (strcmp(recvdata, SELFCHECK_EXPECT) == 0) {
+ /* success */
+ free(recvdata);
+ conn_teardown();
+ return;
+ }
+
+ if (strcmp(recvdata, SELFCHECK_RELOADING) != 0) {
+ ck_abort_msg("Wrong reply for SELFCHECK: '%s' (expected DBUPTODATE or RELOADING)", recvdata);
+ }
+
+ /* still reloading, wait then retry */
+ free(recvdata);
+ recvdata = NULL;
+
+#if defined(_WIN32)
+ Sleep(sleep_ms);
+#else
+ struct timespec ts = { .tv_sec = sleep_ms / 1000,
+ .tv_nsec = (sleep_ms % 1000) * 1000000L };
+ nanosleep(&ts, NULL);
+#endif
+ } while (++attempts < max_attempts);
+
+ ck_abort_msg("SELFCHECK did not reach DBUPTODATE within timeout");
+
+ conn_teardown();
+}
+END_TEST
+
static size_t prepare_instream(char *buf, size_t off, size_t buflen)
{
STATBUF stbuf;
@@ -869,6 +927,7 @@ static Suite *test_clamd_suite(void)
#endif
tcase_add_test(tc_commands, test_stats);
+ tcase_add_test(tc_commands, test_selfcheck);
tcase_add_test(tc_commands, test_instream);
tcase_add_test(tc_commands, test_idsession);
diff --git a/win32/conf_examples/clamd.conf.sample b/win32/conf_examples/clamd.conf.sample
index 8ced1d16f..7973921c3 100644
--- a/win32/conf_examples/clamd.conf.sample
+++ b/win32/conf_examples/clamd.conf.sample
@@ -123,6 +123,11 @@ TCPAddr localhost
# Default: yes
#EnableReloadCommand no
#
+# Enable the SELFCHECK command
+# Setting this to no prevents a client to running selfcheck on clamd.
+# Default: yes
+#EnableSelfCheckCommand no
+#
# Enable the STATS command
# Setting this to no prevents a client from querying statistics.
# Default: yes