Repository navigation
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Note
TLDR: one of the test still failed even after my last round of making them less flaky and worked after re-run. That was caused by a race in our code around Clamav unofficial sigs integration. This also indicated a real issue: if cronjob executing these fails the failure goes to undeliverable email that no one sees. This PR is to fix these.
@diroots since you worked in ajacent area, do you mind having a look?
default.batschecks rspamd's counters after the fixtures: 9 messages scanned, 7 with action "no action". In the overlay's CI they once came out wrong (run 39000469943, attempt 2). The retry passed, and the inputs were identical to a passing run. The failure showed onlystatus : 1, output : (empty), because the tests pipedrspamc statthroughgrep, so the actual counts were lost.The extra message is mail that cron sends itself.
setup.shinstalls the clamav-unofficial-sigs cron entry at a random minute, chosen afresh at each container start. The entry has noMAILTOand no output redirect. Whenever the job prints anything, cron mails it toclamav@<FQDN>throughsendmail.non_smtpd_miltersthen hands that mail to rspamd, which counts it as one more message with "no action". Reproduced locally: a run whose counters read scanned 10 / no action 8 had, in rspamd's/history, one row beyond the fixtures: fromclamav@mail.domain.tld, subjectCron <clamav@mail> [ -x /usr....That run was on rspamd 4.1.5, so this is not new with 4.2.2. The expected rate is about one run in sixty: the window between rspamd starting and the counters being read is under a minute, so it crosses at most one minute boundary, and the cron minute matches it with probability 1/59.
Looking at what the job prints turned up two real faults, both described below: the hourly job cannot write its own state, and it cannot reload clamd.
What is the current behavior (you can also link to an open issue here) ?
The update at startup runs as root, while cron runs the hourly update as
clamav. The hourly run cannot write the state files the startup run created. It cannot reload clamd either, becauses6-svc -2 /services/clamdis root-only. It reports both failures only in its output, and cron mails that output to an address nobody reads, passing through rspamd on the way.What is the new behavior (if this is a feature change) ?
Both updates run as
clamav. clamd is reloaded through its socket, once it has finished starting. The hourly job's output goes to syslog: shell errors atmail.err, the script's own errors and alerts atmail.warning. Both appear indocker logs, and cron sends no mail. WithDISABLE_CLAMAV=truethe updater is not installed at all.Run the clamav-unofficial-sigs update as clamav
rootfs/services/_parent/runruns the startup update as root. cron runs the hourly one asclamav, the script's defaultcron_user. The startup run leavesconfigs/*.txtowned by root, mode 0644, on the/var/mailvolume. Every hourly run then fails to writecurrent-dbs.txt,previous-dbs.txtanddb-changes.txt, and prints aPermission deniedline for each.Once a source's update interval has passed, the hourly run also cannot record the new
last-<source>-update.txt. The stored time never advances, so from then on that source is fetched on every hourly run instead of once per interval. Measured for Sanesecurity, whose interval is 2 h: with its stamp backdated, two consecutive cron runs both ran the Sanesecurity update, and the stamp stayed where it was. The other sources store their stamps the same way.The reload had the same split.
os.confsetsclamd_reload_opt="s6-svc -2 /services/clamd", and s6's control fifo is root-only. Asclamav:with exit code 111. The script only checks the reload's output for the word
ERROR, so it treats this as a success. Since that setting arrived in #46, an hourly update can never have reloaded clamd.Where clamd runs, the effect is a delay rather than signatures never being loaded. clamd's
SelfCheckre-reads changed databases on its own. WithSelfCheckshortened to 20 s, touching one database file was picked up at the next check:This image sets
SelfCheck 3600, so new signatures from the hourly update arrive up to an hour late instead of straight away.The changes:
s6-setuidgid clamav, the same wayencryption.shdrops tozeyple.clamd_reload_opt="clamdscan --reload", the default in the script's ownmaster.conf, which upstream's Debian config does not override either. It talks to clamd'sLocalSocket(mode 666), so it works asclamav. A failed reload now printsERRORand triggers the script's fallback,kill -USR2on clamd's pid, which also works asclamavbecause clamd runs asclamav(except while clamd is starting, see below).clamdscanwas not installed, so the Dockerfile adds it. It is one package (59 KB download, 222 KB installed), and its dependencies (clamav-base,libclamav12) are already in the image.setup.shre-owns the updater's state forclamav, so volumes that already hold root-owned files keep working.user.confis the administrator's file and keeps its owner.fixtures_defaultnow waits for the startup update to finish. It used to check the update's pid file about 0.1 s afterdocker run, before the update had started, so it never actually waited.Log the clamav-unofficial-sigs cron output instead of mailing it
cron mails a job's output to the job's user. Here that is
clamav, which postfix completes toclamav@<FQDN>(myorigin). The FQDN is not inmydestinationand normally is not one of the virtual domains, so postfix tries to relay the mail. In the test container that fails, and the bounce is dropped:With no terminal, which is how cron runs it, the script silences its progress messages but still prints its errors and alerts, and the shell's own errors appear on stderr. Observed causes of output:
Permission deniedlines on every hourly run, from the previous section.ERROR: Only one instance can run at the same time.when cron's minute comes round while the startup update still holds the pid file. The script prints this and exits immediately, so the mail goes out at once. This is the trigger that fits CI's window.The cron entry now runs
/usr/local/bin/clamav-unofficial-sigs-cron.sh, set through the script's owncron_script_full_pathoption, so--install-cronstill generates the file. The wrapper:logger -p mail.errand its stdout tologger -p mail.warning, taggedclamav-unofficial-sigs. rsyslog already copiesmail.*todocker logsandmail.log, warnings tomail.warnand errors tomail.err, which are the places the README sends people to.=/*/-/+the script frames its messages with.mail.infoline when another update is already running. The startup update and the hourly one are independent, so that overlap is expected and should not read as an error. It would otherwise put: ERROR:intomail.log, which the log test checks for.Expected volume, measured with the first section applied: an hour with nothing due prints nothing, and an hour with every source due prints nothing either. The version check prints the two ALERTs (two lines once framing is dropped) every 12 hours, for as long as the bundled script is outdated.
mail.errstays empty unless something is actually broken.What still does not surface are the script's WARNINGs, which it silences under cron. Two of them fail every time their source is due and remain only in its own log,
/var/log/clamav-unofficial-sigs/clamav-unofficial-sigs.log:urlhaus.ndbnever updates, and Yara'sbank_rule.yarreturns 404 upstream. Both concern the bundled script's version.Skip the clamav-unofficial-sigs updater when ClamAV is disabled
With
DISABLE_CLAMAV=true,_parent/runalready skips the startup update, butsetup.shstill installed the hourly cron entry whenever a sigsuser.confexisted. Theldap2suite runs exactly this configuration. Measured with it:ERROR: working directory (work_dir) not writable /var/lib/clamav-unofficial-sigsand downloaded nothing. Without the root startup run, nothing hands the work directory toclamav. cron mailed that error every hour.clamdscancould not reach the socket, there was no pid file for thekill -USR2fallback, and the restart fallback'ss6-svcis root-only.There is nothing for the updater to do without ClamAV, so
setup.shnow skips it entirely and says so at startup ([INFO] clamav-unofficial-sigs is disabled (ClamAV is disabled)).Wait for clamd to finish starting before reloading it
Found while testing the 8.0.0 upgrade (#93), and older than this PR.
_parent/runstarts freshclam, which starts clamd, and runs the startup update at the same time. The update can finish while clamd is still loading its databases, and then neither way of reloading works:clamdscan --reloadfails, because clamd creates its socket only after loading and compiling the databases:ERROR: Could not connect to clamd on LocalSocket /var/run/clamav/clamd.ctl: No such file or directory, exit 2. (clamd/server-th.cline 1517). While it loads,/proc/<pid>/statusshows SIGUSR2 inSigIgn.The script then logs
ClamAV databases reloading, though nothing reloaded. Master loses the same reload, silently: itss6-svc -2sends the same SIGUSR2 and prints nothing. Reproduced on master: clamd kept 3801371 signatures until reloaded by hand, then had 3834389.SelfCheckdoes not make up for it. clamd records the state it compares against after its initial load (clamd.cline 679), so a database installed while that load was reading the directory is never flagged as changed. Shown with a marker database installed during the load andSelfCheckset to 30 s: three checks in a row reportedDatabase status OK, and the marker was only detected after a reload. Such a database waits for the next freshclam update or the next hourly update that installs something.It needs a TTY. Without one the script sleeps 60–600 s before downloading, so clamd has long finished starting. The suites run with
-t, and the reload was lost in 2 of the 4 fresh starts inspected. The sampledocker-composefile sets no TTY. Outside startup, a reload that arrives while clamd is already reloading is merged into it, but there the next SelfCheck does catch whatever that reload missed, so that case is left alone.clamd_reload_optnow runsclamav-unofficial-sigs-reload.sh, which waits for clamd to answer (clamdscan --ping, up to 300 s) and then runsclamdscan --reload. It does this for every reload, so it also covers an hourly update that coincides with clamd restarting, which is the same window as startup. The wait is silent: each failed ping prints anERROR, which the script would take for a failed reload.--waitand--pinghave no effect combined with--reloaditself, so it has to be a separate step.Show the full rspamd stats when a counter test fails
The counter tests used
rspamc stat | grep -i '...'. A wrong count and a failingrspamclooked the same in the output,output : (empty), because rspamc prints its errors to stdout andgrepswallowed them. They now useassert_output --partial, which prints the wholerspamc statoutput on failure.Remove the test containers' volumes with them
mysql:8,postgresandrediskeep their data in anonymous volumes. The init targets remove the previous container withdocker rm -f, which leaves its volume behind, so every suite run leaked one database's data. While working on this, repeated local runs left dozens of these behind and, together with the image builds, filled a 38 GB disk. All removals now usedocker rm -fv, which removes a container's own anonymous volumes and never bind mounts or named volumes.make cleanis untouched.Not in this PR
The bundled clamav-unofficial-sigs is 7.2.5, released 2021-03-20. 8.0.0, released 2026-07-02, is the only release since. According to its release notes it fixes urlhaus (a missing work directory and a cleanup pass that deleted
urlhaus.ndbafter every install) and removesbank_rule.yar. Upgrading means a new script and a config migration, so it is better done separately. The reload change here is still needed with 8.0.0: its new socket fallback runs only when the reload output containsERROR(line 1413), the same check as 7.2.5, and thes6-svcfailure prints noERROR.Type of change
Status
Todo List
How has this been tested ?
The full bats suite, one suite at a time and not as root:
git submodule update --init --recursive,make build, thenmake default,make reverse TEST_DNS=9.9.9.9,make ldap,make ldap2 TEST_DNS=9.9.9.9,make sieve,make ecdsa,make traefik_acmev1,make traefik_acmev2,make traefik_v3.default— 134 tests (128 + 6 new)reverse— 81 testsldap— 66 testsldap2— 30 tests (29 + 1 new)sieve— 2 testsecdsa— 4 teststraefik_acmev1— 11 teststraefik_acmev2— 12 teststraefik_v3— 9 tests349 tests, 0 failures, on the overlay this branch builds on (rspamd 4.1.5).
defaultalso passed twice, 133/133, on an overlay built with rspamd 4.2.2 (AndrewSav/debian-mail-overlayf828c42). That was before the last two commitx, which only changes theDISABLE_CLAMAV=truecase. The second commit, which changes who runs the update but not yet where its output goes, was built and tested on its own as well:default130/130.Repeated clean runs cannot show the flake is gone, because before this change about sixty runs in a row would pass on average. The six new tests in
default.batsrun the cron job on purpose instead, exactly as its cron file says, as its cron user and with no terminal:mail.err. On master it prints the threePermission deniedlines.Database correctly reloaded. On master:Permission denied, exit 111.Only one instanceERROR and exits 1.Permission deniedlands inmail.errand nothing is printed. On master it is printed, which means mailed.Logging Disabledwarning lands inmail.warnand not inmail.err. On master it is printed.ERROR, and be followed byDatabase correctly reloaded. Before the last commit:ERROR: Could not connect to clamd on LocalSocket /var/run/clamav/clamd.ctl: No such file or directory, exit 2.The last two deliberately break the job, so they run after the log tests and do not disturb them.
ldap2.batsgains one test, no cron task when ClamAV is disabled: the suite mounts a sigsuser.confwithDISABLE_CLAMAV=true, and the cron file must not exist. On master it exists.