Skip to content

Commit 57703eb

Browse files
committed
[Fix] cron.daily flock lock leaked to backgrounded scans; switched to
CLOEXEC command form (flock -n FILE "$0") so children never inherit the lock fd [Fix] cron.watchdog version update now runs regardless of sigup result [Fix] README.md md5v2.dat format corrected to HASH:SIZE:{MD5}sig.name.N [New] SHA-256 checksum verification for YARA-X binary in Dockerfile.yara-x [New] test coverage for clean() YARA rescan and YARA(cav) display (3 tests) [New] watchdog sigup-failure resilience test; cron CLOEXEC lock test [Change] Rocky Linux 10 added to CI matrix (9-target); Dockerfile.rocky10 fixed for rockylinux/rockylinux:10 base image and package conflicts
1 parent 20978c7 commit 57703eb

14 files changed

Lines changed: 157 additions & 62 deletions

.github/workflows/smoke-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
timeout-minutes: 15
1212
strategy:
1313
matrix:
14-
os: [debian12, centos6, centos7, rocky8, rocky9, ubuntu2004, ubuntu2404, yara-x]
14+
os: [debian12, centos6, centos7, rocky8, rocky9, rocky10, ubuntu2004, ubuntu2404, yara-x]
1515
fail-fast: false
1616
steps:
1717
- uses: actions/checkout@v4

CHANGELOG

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
v2.0.1 | Feb 22 2026:
2+
[Fix] cron.daily flock lock leaked to backgrounded maldet -b scans via
3+
inherited fd, preventing next day's cron.daily from running; switched
4+
from fd-based locking (exec 9>/flock -n 9) to flock command form
5+
(flock -n FILE "$0") which uses CLOEXEC so children never inherit
6+
the lock fd
7+
[New] SHA-256 checksum verification for YARA-X binary download in
8+
Dockerfile.yara-x
9+
[Fix] cron.watchdog version update (maldet -d) now runs regardless of
10+
signature update result; previously gated on sigup success
11+
[Fix] README.md md5v2.dat format corrected to HASH:SIZE:{MD5}sig.name.N;
12+
SIZE field (file size in bytes) was missing from description
13+
[New] test coverage for clean() YARA rescan path and YARA(cav) signature
14+
count display label (3 tests)
15+
[Change] Rocky Linux 10 added to CI matrix, Makefile, and run-tests.sh;
16+
9-target CI matrix (was 8); Dockerfile.rocky10 fixed: base image
17+
changed to rockylinux/rockylinux:10, --allowerasing for coreutils
18+
conflict, optional inotify-tools/yara separated from required packages
219
[Change] scan_stage_yara() code duplication eliminated: extracted
320
_yara_scan_rules() helper to consolidate text rules and compiled
421
rules scan/parse blocks (~80 duplicate lines removed)
@@ -182,7 +199,7 @@ v2.0.1 | Feb 22 2026:
182199
MD5 scanning, HEX scanning, quarantine, ignore files, config options, cron,
183200
reporting, signatures, file list generation, string length analysis, logging,
184201
purge, compat.conf migrations, hook scanning, resource control, and version
185-
[New] GitHub Actions CI workflow with 8-target matrix: CentOS 6/7, Rocky 8/9,
202+
[New] GitHub Actions CI workflow with 9-target matrix: CentOS 6/7, Rocky 8/9/10,
186203
Ubuntu 20.04/24.04, Debian 12, YARA-X
187204
[New] test coverage for clean operations, ClamAV integration, cron daily, and
188205
alerting (4 new test files, ~40 tests)

CHANGELOG.RELEASE

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
11
v2.0.1 | Feb 22 2026:
2+
[Fix] cron.daily flock lock leaked to backgrounded maldet -b scans via
3+
inherited fd, preventing next day's cron.daily from running; switched
4+
from fd-based locking (exec 9>/flock -n 9) to flock command form
5+
(flock -n FILE "$0") which uses CLOEXEC so children never inherit
6+
the lock fd
7+
[New] SHA-256 checksum verification for YARA-X binary download in
8+
Dockerfile.yara-x
9+
[Fix] cron.watchdog version update (maldet -d) now runs regardless of
10+
signature update result; previously gated on sigup success
11+
[Fix] README.md md5v2.dat format corrected to HASH:SIZE:{MD5}sig.name.N;
12+
SIZE field (file size in bytes) was missing from description
13+
[New] test coverage for clean() YARA rescan path and YARA(cav) signature
14+
count display label (3 tests)
15+
[Change] Rocky Linux 10 added to CI matrix, Makefile, and run-tests.sh;
16+
9-target CI matrix (was 8); Dockerfile.rocky10 fixed: base image
17+
changed to rockylinux/rockylinux:10, --allowerasing for coreutils
18+
conflict, optional inotify-tools/yara separated from required packages
219
[Change] scan_stage_yara() code duplication eliminated: extracted
320
_yara_scan_rules() helper to consolidate text rules and compiled
421
rules scan/parse blocks (~80 duplicate lines removed)
@@ -173,7 +190,7 @@ v2.0.1 | Feb 22 2026:
173190
MD5 scanning, HEX scanning, quarantine, ignore files, config options, cron,
174191
reporting, signatures, file list generation, string length analysis, logging,
175192
purge, compat.conf migrations, hook scanning, resource control, and version
176-
[New] GitHub Actions CI workflow with 8-target matrix: CentOS 6/7, Rocky 8/9,
193+
[New] GitHub Actions CI workflow with 9-target matrix: CentOS 6/7, Rocky 8/9/10,
177194
Ubuntu 20.04/24.04, Debian 12, YARA-X
178195
[New] test coverage for clean operations, ClamAV integration, cron daily, and
179196
alerting (4 new test files, ~40 tests)

PLAN.md

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Organized by phase. Each phase is independent and can be shipped separately.
44
Items marked ~~RESOLVED~~ are completed and kept for reference.
55

6-
**Status:** Phases 1-4 complete. 4 open items in Phase 5.
6+
**Status:** Phases 1-5 complete. All items resolved.
77

88
---
99

@@ -38,37 +38,22 @@ unused yarac discovery removal (4.4).
3838

3939
# Phase 5 — CI / Infrastructure
4040

41-
Test and documentation improvements. 4 open items, all Low severity.
41+
Test and documentation improvements. All items resolved.
4242

43-
## 5.1 rocky10 and ubuntu2204 Dockerfiles not in CI/Makefile
43+
## ~~5.1 rocky10 and ubuntu2204 Dockerfiles not in CI/Makefile~~ RESOLVED
4444

45-
**Severity:** Low
46-
**Problem:** `Dockerfile.rocky10` and `Dockerfile.ubuntu2204` exist but are absent
47-
from CI matrix, Makefile, and `run-tests.sh`.
45+
Rocky Linux 10 added to CI matrix, Makefile, and run-tests.sh (9-target matrix).
46+
Ubuntu 22.04 intentionally excluded.
4847

49-
**Fix:** Add to all three, or remove Dockerfiles if support is not intended.
48+
## ~~5.2 No SHA-256 checksum for YARA-X binary in Dockerfile.yara-x~~ RESOLVED
5049

51-
**Location:** `.github/workflows/smoke-test.yml`, `tests/Makefile`, `tests/run-tests.sh`
50+
Added `sha256sum -c` verification after downloading YARA-X v1.13.0 binary.
5251

53-
## 5.2 No SHA-256 checksum for YARA-X binary in Dockerfile.yara-x
52+
## ~~5.3 No test coverage for clean() YARA rescan or YARA(cav) display~~ RESOLVED
5453

55-
**Severity:** Low
56-
**Problem:** Pre-built binary downloaded from GitHub without checksum verification.
57-
CI-only blast radius but poor optics for a malware scanner project.
58-
59-
**Fix:** Add `sha256sum -c` verification after download.
60-
61-
**Location:** `tests/Dockerfile.yara-x`
62-
63-
## 5.3 No test coverage for clean() YARA rescan or YARA(cav) display
64-
65-
**Severity:** Low
66-
**Problem:** No test exercises the `clean()` YARA rescan path. No test verifies
67-
`YARA(cav)` label when `scan_yara=0`.
68-
69-
**Fix:** Add tests now that Phase 1 fixes have landed.
70-
71-
**Location:** `tests/23-yara.bats`
54+
Added 3 tests to `tests/23-yara.bats`: clean() YARA rescan path exercised,
55+
YARA(cav) label verified when scan_yara=0, plain YARA label verified when
56+
scan_yara=1.
7257

7358
## ~~5.4 No test coverage for import_custsigs_yara_url validation~~ RESOLVED
7459

@@ -81,17 +66,9 @@ Added 3 tests to `tests/23-yara.bats`: corrupt compiled.yarc skipped with
8166
warning, valid compiled.yarc used in scan (yarac skip guard), scan completes
8267
normally without compiled.yarc.
8368

84-
## 5.6 README.md md5v2.dat format description missing SIZE field
85-
86-
**Severity:** Low
87-
**Problem:** Section 8 signature table says md5v2.dat format is
88-
`HASH:{MD5}sig.name.N` but actual format is `HASH:SIZE:{MD5}sig.name.N`. The
89-
SIZE field (file size in bytes) is missing from the description. Pre-existing
90-
error from the original README.md creation.
91-
92-
**Fix:** Correct format column to `HASH:SIZE:{MD5}sig.name.N`.
69+
## ~~5.6 README.md md5v2.dat format description missing SIZE field~~ RESOLVED
9370

94-
**Location:** `README.md` section 8, line 439
71+
Corrected format column to `HASH:SIZE:{MD5}sig.name.N` in README.md section 8.
9572

9673
---
9774

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ LMD ships with three signature types:
436436

437437
| Type | File | Format | Count |
438438
|------|------|--------|-------|
439-
| MD5 hashes | `sigs/md5v2.dat` | `HASH:{MD5}sig.name.N` | ~14,801 |
439+
| MD5 hashes | `sigs/md5v2.dat` | `HASH:SIZE:{MD5}sig.name.N` | ~14,801 |
440440
| HEX patterns | `sigs/hex.dat` | `HEXSTRING:{HEX}sig.name.N` | ~2,054 |
441441
| YARA rules | `sigs/rfxn.yara` | YARA syntax | ~783 rules |
442442
| Compiled YARA | `sigs/compiled.yarc` | `yarac` output | optional |

cron.daily

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ export LMDCRON=1
44
inspath='/usr/local/maldetect'
55
intcnf="$inspath/internals/internals.conf"
66

7-
# prevent overlapping cron runs
7+
# prevent overlapping cron runs — use flock command form so the lock fd is
8+
# CLOEXEC and never inherited by backgrounded maldet -b scans
89
LOCKFILE="$inspath/tmp/.cron.lock"
9-
if command -v flock >/dev/null 2>&1; then
10-
exec 9>"$LOCKFILE"
11-
if ! flock -n 9; then
12-
exit 0
13-
fi
10+
if command -v flock >/dev/null 2>&1 && [ -z "$_CRON_FLOCK" ]; then
11+
export _CRON_FLOCK=1
12+
flock -n "$LOCKFILE" "$0" "$@" || :
13+
exit 0
1414
fi
1515

1616
if [ -f "$intcnf" ]; then

cron.watchdog

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,8 @@ echo "$(date) maldet-watchdog: signatures are ${sig_age_days}d old (threshold: $
4747
$inspath/maldet -u 2>&1 | tail -5 >> "$event_log"
4848
sig_update_rc=${PIPESTATUS[0]}
4949

50-
# If sig update succeeded, also try version update
51-
if [ "$sig_update_rc" -eq 0 ]; then
52-
$inspath/maldet -d 2>&1 | tail -5 >> "$event_log"
53-
fi
50+
# Run version update regardless of signature update result
51+
$inspath/maldet -d 2>&1 | tail -5 >> "$event_log"
52+
ver_update_rc=${PIPESTATUS[0]}
5453

55-
echo "$(date) maldet-watchdog: emergency update completed (sigup rc=$sig_update_rc)" >> "$event_log"
54+
echo "$(date) maldet-watchdog: emergency update completed (sigup rc=$sig_update_rc, verup rc=$ver_update_rc)" >> "$event_log"

tests/20-cron.bats

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ MOCK
153153
exec 8>&-
154154
}
155155

156+
@test "cron lock uses CLOEXEC command form (no fd leak)" {
157+
# Verify cron.daily uses flock command form (_CRON_FLOCK guard)
158+
run grep '_CRON_FLOCK' /etc/cron.daily/maldet
159+
assert_success
160+
# Verify no exec 9> fd-based locking (old pattern that leaked to children)
161+
run grep 'exec 9>' /etc/cron.daily/maldet
162+
assert_failure
163+
}
164+
156165
@test "cron sources custom config file" {
157166
lmd_set_config cron_daily_scan 0
158167
mkdir -p "$LMD_INSTALL/cron"

tests/22-updates.bats

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,38 @@ run_cron_daily() {
524524
run bash /etc/cron.weekly/maldet-watchdog
525525

526526
assert_success
527-
# Should have called maldet -u
527+
# Should have called both maldet -u and maldet -d
528528
[ -f "$CRON_MALDET_LOG" ]
529529
run grep "MALDET_CALL:.*-u" "$CRON_MALDET_LOG"
530530
assert_success
531+
run grep "MALDET_CALL:.*-d" "$CRON_MALDET_LOG"
532+
assert_success
533+
}
534+
535+
@test "watchdog runs version update even when signature update fails" {
536+
[ -f /etc/cron.weekly/maldet-watchdog ] || skip "watchdog not installed"
537+
touch -d '10 days ago' "$LMD_INSTALL/sigs/maldet.sigs.ver"
538+
# Install mock maldet that fails on -u but succeeds on -d
539+
cp "$LMD_INSTALL/maldet" "$LMD_INSTALL/maldet.real"
540+
cat > "$LMD_INSTALL/maldet" <<'MOCK'
541+
#!/usr/bin/env bash
542+
echo "MALDET_CALL: $@" >> /tmp/cron-maldet.log
543+
case "$1" in
544+
-u) exit 1 ;;
545+
*) exit 0 ;;
546+
esac
547+
MOCK
548+
chmod 755 "$LMD_INSTALL/maldet"
549+
550+
run bash /etc/cron.weekly/maldet-watchdog
551+
552+
assert_success
553+
[ -f "$CRON_MALDET_LOG" ]
554+
# Both -u and -d should have been called despite -u failure
555+
run grep "MALDET_CALL:.*-u" "$CRON_MALDET_LOG"
556+
assert_success
557+
run grep "MALDET_CALL:.*-d" "$CRON_MALDET_LOG"
558+
assert_success
531559
}
532560

533561
@test "watchdog logs staleness warning to event_log" {

tests/23-yara.bats

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,44 @@ EOF
416416
run grep "failed validation" "$LMD_INSTALL/logs/event_log"
417417
assert_failure
418418
}
419+
420+
# ── Group 9: clean() YARA Rescan & YARA(cav) Display ──
421+
422+
@test "YARA: clean() invokes YARA rescan after cleaning" {
423+
# HEX sig matching infected-base64.php with clean script name
424+
echo "6576616c286261736536345f6465636f646528:base64.inject.unclassed.99" \
425+
> "$LMD_INSTALL/sigs/custom.hex.dat"
426+
# Suppress builtin sigs that match the same pattern but have no clean script
427+
echo "php.base64.inject" > "$LMD_INSTALL/ignore_sigs"
428+
lmd_set_config quarantine_hits 1
429+
lmd_set_config quarantine_clean 1
430+
lmd_set_config scan_yara 1
431+
cp "$SAMPLES_DIR/infected-base64.php" "$TEST_SCAN_DIR/"
432+
> "$LMD_INSTALL/logs/event_log"
433+
maldet -a "$TEST_SCAN_DIR" || true
434+
# clean() should have logged rescanning entries
435+
run grep "{clean}" "$LMD_INSTALL/logs/event_log"
436+
assert_success
437+
# Verify the YARA rescan message specifically
438+
run grep "{clean} rescanning" "$LMD_INSTALL/logs/event_log"
439+
assert_success
440+
}
441+
442+
@test "YARA: signature count shows YARA(cav) when scan_yara=0" {
443+
lmd_set_config scan_yara 0
444+
cp "$SAMPLES_DIR/clean-file.txt" "$TEST_SCAN_DIR/"
445+
run maldet -a "$TEST_SCAN_DIR"
446+
assert_success
447+
assert_output --partial "YARA(cav)"
448+
}
449+
450+
@test "YARA: signature count shows YARA (not cav) when scan_yara=1" {
451+
source /opt/tests/helpers/create-yara-sigs.sh
452+
lmd_set_config scan_yara 1
453+
cp "$SAMPLES_DIR/clean-file.txt" "$TEST_SCAN_DIR/"
454+
run maldet -a "$TEST_SCAN_DIR"
455+
assert_success
456+
# Should show plain YARA label, not YARA(cav)
457+
refute_output --partial "YARA(cav)"
458+
assert_output --partial "YARA |"
459+
}

0 commit comments

Comments
 (0)