[Pkg-clamav-devel] Bug#1143939: clamav: I prepared package 1.4.6+dfsg-1 for #1143939
Edmund Lodewijks
edmund at proteamail.com
Tue Aug 18 06:20:40 BST 2026
On Mon, 17 Aug 2026 20:06:35 +0200 Edmund Lodewijks <edmund at proteamail.com> wrote:
> A debdiff is attached to this email.
Forgot to attach a proper debdiff, so here it is.
--
Edmund Lodewijks <edmund at proteamail.com>
TZ: UTC+2 / GMT+2
-------------- next part --------------
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/build.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/build.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/build.rs 1970-01-01 02:00:00.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/build.rs 2006-07-24 03:21:28.000000000 +0200
@@ -0,0 +1,14 @@
+// The rustc-cfg emitted by the build script are *not* public API.
+
+use std::env;
+
+fn main() {
+ println!("cargo:rerun-if-changed=build.rs");
+ println!("cargo:rustc-check-cfg=cfg(crossbeam_sanitize_thread)");
+
+ // `cfg(sanitize = "..")` is not stabilized.
+ let sanitize = env::var("CARGO_CFG_SANITIZE").unwrap_or_default();
+ if sanitize.contains("thread") {
+ println!("cargo:rustc-cfg=crossbeam_sanitize_thread");
+ }
+}
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/.cargo-checksum.json clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/.cargo-checksum.json
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/.cargo-checksum.json 2026-07-01 08:46:05.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/.cargo-checksum.json 2026-08-06 23:12:24.000000000 +0200
@@ -1 +1 @@
-{"files":{".cargo_vcs_info.json":"49529baffd2fd52cbf26931156570b752c8fc679c09a5091ba11c48645c77754","CHANGELOG.md":"678c7c5b4e522345076d63e8f24f7ab4dc9f6b7428ca2f665e4017f7ef24a087","Cargo.lock":"fd85f51f6b4a2dabbb41d9f96775abd21d89221fa01c154afac970c539022f17","Cargo.toml":"cfbceb820c7a1519351826839decd3ff1b1ad54ef2c4dfc4d2c9f173e4726046","Cargo.toml.orig":"b3c80c139498aac55f4caabab81579a87169f6b92156312ebcaec61f5578af9d","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"5734ed989dfca1f625b40281ee9f4530f91b2411ec01cb748223e7eb87e201ab","README.md":"6ba897c52496a66705df72da33dea5f6e0ce5caa87c4ff073b0faf4e05516dad","benches/defer.rs":"c330b704d96b2ad1aed29f72c37a99da534adef8cb06a3976d5f93bf567abb20","benches/flush.rs":"0389ac6c473632f0e93c962f223404cc360257f6699b4ec90b9b3be16bb6d74f","benches/pin.rs":"2f649a5153745c7930efdb32a52f9dc522f7b8cf548a251c5e2c82ee25dc3fff","examples/sanitize.rs":"a39d1635fa61e643e59192d7a63becc97ff81f03c1f4e03d38cedefb1525026a","src/atomic.rs":"48b8b02d1e0235b2d87342c13c09d778fba076f79addef32294bed5b8f67b21a","src/collector.rs":"df05c7573413a8f3ac933de7cf941d24bd0ca7341f5923dcad2f811a020c49eb","src/default.rs":"8196e9a2a7a43fdd668177585ba1d4deaec2d16a8a9532f819e4d9afd64ca73d","src/deferred.rs":"092c49e65d5f0ccad8c868b9bcaf431b580c98b7efed98c3797d82d0b9d0c471","src/epoch.rs":"e6813975198df667423c7e1911f7a0f5cb3a917e56080eecd6250d9cca7af950","src/guard.rs":"8db7a20503f55e9e29fc1cf33f99522ec0a5873683ab16638e0e55c917bfc30a","src/internal.rs":"74a15b34b235ab428ffa41cb3a01930e29e3f91e35a288f8f6e0c3c2f56e63f6","src/lib.rs":"3f81f1727c3f74114fbd2f9225a4899834fc254f1444f7c7355901c8fd755494","src/sync/list.rs":"10aa4c59845ab9ff1d8bcb6f594b70bbe23c320fa7a2b125fdf85df88b9d61e2","src/sync/mod.rs":"326e32489d467e974c441120640a8338aa55da55c24b20276075ce9053997326","src/sync/once_lock.rs":"aa8f957604d1119c4fc7038a18c14a6281230e81005f31201c099acff284ad4b","src/sync/queue.rs":"d4ad500501c52a90b6624dc31196793be09bd19e9c298d5dd7b3ae37bee6b6a8","tests/loom.rs":"db772f4478966de6ec98774ca4093171dc942da635822a0d2d3257d31188cb9b"},"package":"5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"}
\ No newline at end of file
+{"files":{".cargo_vcs_info.json":"419f1b11148246c8db81f32a3476d51490dd1d5251d343c42a3a66c4b7c4638c","CHANGELOG.md":"4c9eb194b40755d85bc51cad9d1b8ccec3136e0abacf364c6e579128c8632aab","Cargo.lock":"877dfecb029677e7c4f08c55f171c77a81f8572ace18110450611aa58faedc8a","Cargo.toml":"aa33b807f5f74c0336045aecd27e90f9f4c8e0fceceb3967fdc154237b76c3a6","Cargo.toml.orig":"09f371f8080f375812e8c8a9ead76bb33d85c25e8bacd8c8803eef3f30d047ac","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"5734ed989dfca1f625b40281ee9f4530f91b2411ec01cb748223e7eb87e201ab","README.md":"6ba897c52496a66705df72da33dea5f6e0ce5caa87c4ff073b0faf4e05516dad","benches/defer.rs":"c330b704d96b2ad1aed29f72c37a99da534adef8cb06a3976d5f93bf567abb20","benches/flush.rs":"0389ac6c473632f0e93c962f223404cc360257f6699b4ec90b9b3be16bb6d74f","benches/pin.rs":"2f649a5153745c7930efdb32a52f9dc522f7b8cf548a251c5e2c82ee25dc3fff","build.rs":"e40cf96d7d7b1650f9f53a3f578633a178324dbea1d905b3f71a75b45d3982a1","examples/sanitize.rs":"a39d1635fa61e643e59192d7a63becc97ff81f03c1f4e03d38cedefb1525026a","src/atomic.rs":"d738c357e6cc2ddc0b744117e98c21362097ffa5cab1e268df090b7c09c7534d","src/collector.rs":"12c6a00496fd702facaff3efdc993f326049d2bbb21fb2c9db1b8ffe6f54cf69","src/default.rs":"a153392422d2a473afb01873d83c8d92f34a3bab5dde2f3743d916acce36f9b9","src/deferred.rs":"5ad2032c40f96e32f3bab367c843c3091c4300402e938217d2706070f20c6811","src/epoch.rs":"65d910a683413a8939a4c3c2dfd88a5e05b6f4e8f7a26a0abdaed775c7d9e1f2","src/guard.rs":"555bb756fe0c422d7f778121226834158cd143c664211a955662c789c867d6d1","src/internal.rs":"75db50474a2b17c1808746917299fe9bba6657c6d409ada1794c596d8dc0961f","src/lib.rs":"4c7266a7f5f480aed776a78fe221d95e712731527555b78270eda0f44c4ebb44","src/sync/list.rs":"6950177b01fa6f51d3f4ca22f3ce4e77143eed28bec04579ecc8a8599ab17031","src/sync/mod.rs":"326e32489d467e974c441120640a8338aa55da55c24b20276075ce9053997326","src/sync/once_lock.rs":"aa8f957604d1119c4fc7038a18c14a6281230e81005f31201c099acff284ad4b","src/sync/queue.rs":"0324e9b07dfd0f76ba0058395a4b9ae4b4a6e5ab0aec41d98edab7864bac3157","tests/loom.rs":"db772f4478966de6ec98774ca4093171dc942da635822a0d2d3257d31188cb9b"},"package":"2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"}
\ No newline at end of file
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.lock clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.lock
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.lock 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.lock 2006-07-24 03:21:28.000000000 +0200
@@ -4,31 +4,32 @@
[[package]]
name = "aho-corasick"
-version = "1.1.2"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "cc"
-version = "1.0.83"
+version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
- "libc",
+ "find-msvc-tools",
+ "shlex",
]
[[package]]
name = "cfg-if"
-version = "1.0.0"
+version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crossbeam-epoch"
-version = "0.9.18"
+version = "0.9.20"
dependencies = [
"crossbeam-utils",
"loom",
@@ -37,32 +38,39 @@
[[package]]
name = "crossbeam-utils"
-version = "0.8.18"
+version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
dependencies = [
- "cfg-if",
"loom",
]
[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
name = "generator"
-version = "0.7.5"
+version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
+checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
dependencies = [
"cc",
+ "cfg-if",
"libc",
"log",
"rustversion",
- "windows",
+ "windows-link",
+ "windows-result",
]
[[package]]
name = "getrandom"
-version = "0.2.11"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
@@ -71,27 +79,27 @@
[[package]]
name = "lazy_static"
-version = "1.4.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.152"
+version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
-version = "0.4.20"
+version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]]
name = "loom"
-version = "0.7.1"
+version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e045d70ddfbc984eacfa964ded019534e8f6cbf36f6410aee0ed5cefa5a9175"
+checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
@@ -102,76 +110,72 @@
[[package]]
name = "matchers"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
- "regex-automata 0.1.10",
+ "regex-automata",
]
[[package]]
name = "memchr"
-version = "2.7.1"
+version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "nu-ansi-term"
-version = "0.46.0"
+version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "overload",
- "winapi",
+ "windows-sys",
]
[[package]]
name = "once_cell"
-version = "1.19.0"
+version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
-
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
-version = "0.2.13"
+version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "ppv-lite86"
-version = "0.2.17"
+version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
[[package]]
name = "proc-macro2"
-version = "1.0.76"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.35"
+version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
-version = "0.8.5"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
dependencies = [
"libc",
"rand_chacha",
@@ -198,54 +202,27 @@
]
[[package]]
-name = "regex"
-version = "1.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-automata 0.4.3",
- "regex-syntax 0.8.2",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
-]
-
-[[package]]
name = "regex-automata"
-version = "0.4.3"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
- "regex-syntax 0.8.2",
+ "regex-syntax",
]
[[package]]
name = "regex-syntax"
-version = "0.6.29"
+version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
-[[package]]
-name = "regex-syntax"
-version = "0.8.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustversion"
-version = "1.0.14"
+version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "scoped-tls"
@@ -263,16 +240,22 @@
]
[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
name = "smallvec"
-version = "1.11.2"
+version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
-version = "2.0.48"
+version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
@@ -281,41 +264,28 @@
[[package]]
name = "thread_local"
-version = "1.1.7"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
- "once_cell",
]
[[package]]
name = "tracing"
-version = "0.1.40"
+version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
- "tracing-attributes",
"tracing-core",
]
[[package]]
-name = "tracing-attributes"
-version = "0.1.27"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
name = "tracing-core"
-version = "0.1.32"
+version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
@@ -334,14 +304,14 @@
[[package]]
name = "tracing-subscriber"
-version = "0.3.18"
+version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
- "regex",
+ "regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
@@ -352,106 +322,62 @@
[[package]]
name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "valuable"
-version = "0.1.0"
+version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
-name = "winapi"
-version = "0.3.9"
+name = "windows-link"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
+name = "windows-result"
+version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
-name = "windows"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "windows-targets",
+ "windows-link",
]
[[package]]
-name = "windows-targets"
-version = "0.48.5"
+name = "windows-sys"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows-link",
]
[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.48.5"
+name = "zerocopy"
+version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
-[[package]]
-name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
+dependencies = [
+ "zerocopy-derive",
+]
[[package]]
-name = "windows_x86_64_msvc"
-version = "0.48.5"
+name = "zerocopy-derive"
+version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml 2006-07-24 03:21:28.000000000 +0200
@@ -13,7 +13,13 @@
edition = "2021"
rust-version = "1.61"
name = "crossbeam-epoch"
-version = "0.9.18"
+version = "0.9.20"
+build = "build.rs"
+autolib = false
+autobins = false
+autoexamples = false
+autotests = false
+autobenches = false
description = "Epoch-based garbage collection"
homepage = "https://github.com/crossbeam-rs/crossbeam/tree/master/crossbeam-epoch"
readme = "README.md"
@@ -31,13 +37,6 @@
license = "MIT OR Apache-2.0"
repository = "https://github.com/crossbeam-rs/crossbeam"
-[dependencies.crossbeam-utils]
-version = "0.8.18"
-default-features = false
-
-[dev-dependencies.rand]
-version = "0.8"
-
[features]
alloc = []
default = ["std"]
@@ -51,7 +50,54 @@
"crossbeam-utils/std",
]
+[lib]
+name = "crossbeam_epoch"
+path = "src/lib.rs"
+
+[[example]]
+name = "sanitize"
+path = "examples/sanitize.rs"
+
+[[test]]
+name = "loom"
+path = "tests/loom.rs"
+
+[[bench]]
+name = "defer"
+path = "benches/defer.rs"
+
+[[bench]]
+name = "flush"
+path = "benches/flush.rs"
+
+[[bench]]
+name = "pin"
+path = "benches/pin.rs"
+
+[dependencies.crossbeam-utils]
+version = "0.8.18"
+default-features = false
+
+[dev-dependencies.rand]
+version = "0.8"
+
[target."cfg(crossbeam_loom)".dependencies.loom-crate]
version = "0.7.1"
optional = true
package = "loom"
+
+[lints.clippy.declare_interior_mutable_const]
+level = "allow"
+priority = 1
+
+[lints.clippy.lint_groups_priority]
+level = "allow"
+priority = 1
+
+[lints.rust.unexpected_cfgs]
+level = "warn"
+priority = 0
+check-cfg = [
+ "cfg(crossbeam_loom)",
+ "cfg(crossbeam_sanitize)",
+]
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml.orig clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml.orig
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml.orig 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/Cargo.toml.orig 2006-07-24 03:21:28.000000000 +0200
@@ -2,9 +2,9 @@
name = "crossbeam-epoch"
# When publishing a new version:
# - Update CHANGELOG.md
-# - Update README.md
-# - Create "crossbeam-epoch-X.Y.Z" git tag
-version = "0.9.18"
+# - Update README.md (when increasing major or minor version)
+# - Run './tools/publish.sh crossbeam-epoch <version>'
+version = "0.9.20"
edition = "2021"
rust-version = "1.61"
license = "MIT OR Apache-2.0"
@@ -54,3 +54,6 @@
[dev-dependencies]
rand = "0.8"
+
+[lints]
+workspace = true
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/.cargo_vcs_info.json clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/.cargo_vcs_info.json
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/.cargo_vcs_info.json 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/.cargo_vcs_info.json 2006-07-24 03:21:28.000000000 +0200
@@ -1,6 +1,6 @@
{
"git": {
- "sha1": "9c3182abebb36bdc9446d75d4644190fef70fa01"
+ "sha1": "239bae00257967a109911b9ebe7c0554d6333501"
},
"path_in_vcs": "crossbeam-epoch"
}
\ No newline at end of file
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/CHANGELOG.md clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/CHANGELOG.md
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/CHANGELOG.md 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/CHANGELOG.md 2006-07-24 03:21:28.000000000 +0200
@@ -1,3 +1,15 @@
+# Version 0.9.20
+
+- Fix invalid pointer dereference in `fmt::Pointer` impl for `Atomic`/`Shared` when the underlying pointer is invalid. This fixes unsoundness that was not fully addressed in 0.9.19's fix. (#1276)
+
+# Version 0.9.19
+
+- Fix null pointer dereference in `fmt::Pointer` impl for `Atomic`/`Shared` when it is a null pointer. (#1273)
+- Fix return value of `Atomic::fetch_update`. (#1197)
+- Improve compatibility with ThreadSanitizer. (#998)
+- Allow unsized types in `Guard::defer_destroy`. (#1201)
+- Use 64-bit integer for epoch representation on 32-bit platforms if `AtomicU64` is available. (#1230)
+
# Version 0.9.18
- Remove dependency on `cfg-if`. (#1072)
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/atomic.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/atomic.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/atomic.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/atomic.rs 2006-07-24 03:21:28.000000000 +0200
@@ -232,7 +232,6 @@
/// along with pointer as in `Box<[T]>`).
///
/// Elements are not present in the type, but they will be in the allocation.
-/// ```
#[repr(C)]
struct Array<T> {
/// The number of elements (not the number of bytes).
@@ -635,7 +634,7 @@
let mut prev = self.load(fail_order, guard);
while let Some(next) = func(prev) {
match self.compare_exchange_weak(prev, next, set_order, fail_order, guard) {
- Ok(shared) => return Ok(shared),
+ Ok(_result) => return Ok(prev),
Err(next_prev) => prev = next_prev.current,
}
}
@@ -943,7 +942,7 @@
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let data = self.data.load(Ordering::SeqCst);
let (raw, _) = decompose_tag::<T>(data);
- fmt::Pointer::fmt(&(unsafe { T::deref(raw) as *const _ }), f)
+ fmt::Pointer::fmt(&(raw as *const ()), f)
}
}
@@ -1662,7 +1661,8 @@
impl<T: ?Sized + Pointable> fmt::Pointer for Shared<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- fmt::Pointer::fmt(&(unsafe { self.deref() as *const _ }), f)
+ let (raw, _) = decompose_tag::<T>(self.data);
+ fmt::Pointer::fmt(&(raw as *const ()), f)
}
}
@@ -1674,8 +1674,8 @@
#[cfg(all(test, not(crossbeam_loom)))]
mod tests {
- use super::{Owned, Shared};
- use std::mem::MaybeUninit;
+ use super::{Atomic, Owned, Shared};
+ use std::{format, mem::MaybeUninit};
#[test]
fn valid_tag_i8() {
@@ -1695,8 +1695,22 @@
#[test]
fn array_init() {
- let owned = Owned::<[MaybeUninit<usize>]>::init(10);
- let arr: &[MaybeUninit<usize>] = &owned;
+ let mut owned = Owned::<[MaybeUninit<usize>]>::init(10);
+ let arr: &mut [MaybeUninit<usize>] = &mut owned;
+ arr[arr.len() - 1].write(20);
assert_eq!(arr.len(), 10);
}
+
+ #[test]
+ fn format_null() {
+ let atomic = Atomic::<usize>::null();
+ assert_eq!(format!("{atomic:p}"), "0x0");
+ let atomic = Atomic::<[MaybeUninit<usize>]>::null();
+ assert_eq!(format!("{atomic:p}"), "0x0");
+
+ let shared = Shared::<usize>::null();
+ assert_eq!(format!("{shared:p}"), "0x0");
+ let shared = Shared::<[MaybeUninit<usize>]>::null();
+ assert_eq!(format!("{shared:p}"), "0x0");
+ }
}
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/collector.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/collector.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/collector.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/collector.rs 2006-07-24 03:21:28.000000000 +0200
@@ -113,6 +113,7 @@
mod tests {
use std::mem::ManuallyDrop;
use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::vec::Vec;
use crossbeam_utils::thread;
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/default.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/default.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/default.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/default.rs 2006-07-24 03:21:28.000000000 +0200
@@ -76,7 +76,7 @@
}
}
- thread_local! {
+ std::thread_local! {
static FOO: Foo = const { Foo };
}
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/deferred.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/deferred.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/deferred.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/deferred.rs 2006-07-24 03:21:28.000000000 +0200
@@ -90,8 +90,11 @@
#[cfg(all(test, not(crossbeam_loom)))]
mod tests {
use super::Deferred;
+ use std::boxed::Box;
use std::cell::Cell;
use std::convert::identity;
+ use std::string::ToString;
+ use std::vec;
#[test]
fn on_stack() {
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/epoch.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/epoch.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/epoch.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/epoch.rs 2006-07-24 03:21:28.000000000 +0200
@@ -7,7 +7,23 @@
//! If an object became garbage in some epoch, then we can be sure that after two advancements no
//! participant will hold a reference to it. That is the crux of safe memory reclamation.
-use crate::primitive::sync::atomic::{AtomicUsize, Ordering};
+use crate::primitive::sync::atomic::Ordering;
+
+// Ideally, we want to always use AtomicU64, but since it is not available on all platforms,
+// we only use it when it is available for now.
+// TODO: On platforms where AtomicU64 is unavailable, we may want to use AtomicCell instead of AtomicUsize.
+#[cfg(target_has_atomic = "64")]
+type AtomicEpochRepr = crate::primitive::sync::atomic::AtomicU64;
+#[cfg(not(target_has_atomic = "64"))]
+type AtomicEpochRepr = crate::primitive::sync::atomic::AtomicUsize;
+#[cfg(target_has_atomic = "64")]
+type EpochRepr = u64;
+#[cfg(not(target_has_atomic = "64"))]
+type EpochRepr = usize;
+#[cfg(target_has_atomic = "64")]
+type EpochReprSigned = i64;
+#[cfg(not(target_has_atomic = "64"))]
+type EpochReprSigned = isize;
/// An epoch that can be marked as pinned or unpinned.
///
@@ -16,7 +32,7 @@
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
pub(crate) struct Epoch {
/// The least significant bit is set if pinned. The rest of the bits hold the epoch.
- data: usize,
+ data: EpochRepr,
}
impl Epoch {
@@ -30,11 +46,11 @@
///
/// Internally, epochs are represented as numbers in the range `(isize::MIN / 2) .. (isize::MAX
/// / 2)`, so the returned distance will be in the same interval.
- pub(crate) fn wrapping_sub(self, rhs: Self) -> isize {
+ pub(crate) fn wrapping_sub(self, rhs: Self) -> EpochReprSigned {
// The result is the same with `(self.data & !1).wrapping_sub(rhs.data & !1) as isize >> 1`,
// because the possible difference of LSB in `(self.data & !1).wrapping_sub(rhs.data & !1)`
// will be ignored in the shift operation.
- self.data.wrapping_sub(rhs.data & !1) as isize >> 1
+ self.data.wrapping_sub(rhs.data & !1) as EpochReprSigned >> 1
}
/// Returns `true` if the epoch is marked as pinned.
@@ -75,15 +91,15 @@
pub(crate) struct AtomicEpoch {
/// Since `Epoch` is just a wrapper around `usize`, an `AtomicEpoch` is similarly represented
/// using an `AtomicUsize`.
- data: AtomicUsize,
+ data: AtomicEpochRepr,
}
impl AtomicEpoch {
/// Creates a new atomic epoch.
#[inline]
pub(crate) fn new(epoch: Epoch) -> Self {
- let data = AtomicUsize::new(epoch.data);
- AtomicEpoch { data }
+ let data = AtomicEpochRepr::new(epoch.data);
+ Self { data }
}
/// Loads a value from the atomic epoch.
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/guard.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/guard.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/guard.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/guard.rs 2006-07-24 03:21:28.000000000 +0200
@@ -1,7 +1,7 @@
use core::fmt;
use core::mem;
-use crate::atomic::Shared;
+use crate::atomic::{Pointable, Shared};
use crate::collector::Collector;
use crate::deferred::Deferred;
use crate::internal::Local;
@@ -265,7 +265,7 @@
/// }
/// # unsafe { drop(a.into_owned()); } // avoid leak
/// ```
- pub unsafe fn defer_destroy<T>(&self, ptr: Shared<'_, T>) {
+ pub unsafe fn defer_destroy<T: ?Sized + Pointable>(&self, ptr: Shared<'_, T>) {
self.defer_unchecked(move || ptr.into_owned());
}
@@ -273,7 +273,7 @@
/// global cache.
///
/// Call this method after deferring execution of a function if you want to get it executed as
- /// soon as possible. Flushing will make sure it is residing in in the global cache, so that
+ /// soon as possible. Flushing will make sure it is residing in the global cache, so that
/// any thread has a chance of taking the function and executing it.
///
/// If this method is called from an [`unprotected`] guard, it is a no-op (nothing happens).
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/internal.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/internal.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/internal.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/internal.rs 2006-07-24 03:21:28.000000000 +0200
@@ -229,6 +229,11 @@
let global_epoch = self.epoch.load(Ordering::Relaxed);
atomic::fence(Ordering::SeqCst);
+ // For ThreadSanitizer that does not understand fences, we simulate the equivalent effect.
+ // It is unfortunate that allocation is required, but without it, synchronization might
+ // occur in cases where it should not, potentially causing false positives.
+ #[cfg(crossbeam_sanitize_thread)]
+ let mut locals = alloc::vec![];
// TODO(stjepang): `Local`s are stored in a linked list because linked lists are fairly
// easy to implement in a lock-free manner. However, traversal can be slow due to cache
// misses and data dependencies. We should experiment with other data structures as well.
@@ -248,9 +253,17 @@
if local_epoch.is_pinned() && local_epoch.unpinned() != global_epoch {
return global_epoch;
}
+
+ #[cfg(crossbeam_sanitize_thread)]
+ locals.push(local);
}
}
}
+ #[cfg(crossbeam_sanitize_thread)]
+ for local in locals {
+ local.epoch.load(Ordering::Acquire);
+ }
+ #[cfg(not(crossbeam_sanitize_thread))]
atomic::fence(Ordering::Acquire);
// All pinned participants were pinned in the current global epoch.
@@ -556,7 +569,7 @@
#[cfg(all(test, not(crossbeam_loom)))]
mod tests {
- use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::sync::atomic::AtomicUsize;
use super::*;
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/lib.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/lib.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/lib.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/lib.rs 2006-07-24 03:21:28.000000000 +0200
@@ -48,6 +48,7 @@
//! For majority of use cases, just use the default garbage collector by invoking [`pin`]. If you
//! want to create your own garbage collector, use the [`Collector`] API.
+#![no_std]
#![doc(test(
no_crate_inject,
attr(
@@ -61,10 +62,11 @@
rust_2018_idioms,
unreachable_pub
)]
-#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(crossbeam_loom)]
extern crate loom_crate as loom;
+#[cfg(feature = "std")]
+extern crate std;
#[cfg(crossbeam_loom)]
#[allow(unused_imports, dead_code)]
@@ -74,6 +76,8 @@
}
pub(crate) mod sync {
pub(crate) mod atomic {
+ #[cfg(target_has_atomic = "64")]
+ pub(crate) use loom::sync::atomic::AtomicU64;
pub(crate) use loom::sync::atomic::{fence, AtomicPtr, AtomicUsize, Ordering};
// FIXME: loom does not support compiler_fence at the moment.
@@ -119,13 +123,9 @@
}
}
pub(crate) mod sync {
- pub(crate) mod atomic {
- pub(crate) use core::sync::atomic::{
- compiler_fence, fence, AtomicPtr, AtomicUsize, Ordering,
- };
- }
#[cfg(feature = "alloc")]
pub(crate) use alloc::sync::Arc;
+ pub(crate) use core::sync::atomic;
}
#[cfg(feature = "std")]
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/list.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/list.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/list.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/list.rs 2006-07-24 03:21:28.000000000 +0200
@@ -301,6 +301,7 @@
use crate::{Collector, Owned};
use crossbeam_utils::thread;
use std::sync::Barrier;
+ use std::vec::Vec;
impl IsElement<Entry> for Entry {
fn entry_of(entry: &Entry) -> &Entry {
diff -Nru clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/queue.rs clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/queue.rs
--- clamav-1.4.5+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/queue.rs 2006-07-24 03:21:28.000000000 +0200
+++ clamav-1.4.6+dfsg/.cargo/vendor/crossbeam-epoch/src/sync/queue.rs 2006-07-24 03:21:28.000000000 +0200
@@ -219,6 +219,7 @@
use super::*;
use crate::pin;
use crossbeam_utils::thread;
+ use std::vec;
struct Queue<T> {
queue: super::Queue<T>,
diff -Nru clamav-1.4.5+dfsg/Cargo.lock clamav-1.4.6+dfsg/Cargo.lock
--- clamav-1.4.5+dfsg/Cargo.lock 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/Cargo.lock 2026-08-06 23:12:04.000000000 +0200
@@ -393,9 +393,9 @@
[[package]]
name = "crossbeam-epoch"
-version = "0.9.18"
+version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
diff -Nru clamav-1.4.5+dfsg/clamav-config.h.cmake.in clamav-1.4.6+dfsg/clamav-config.h.cmake.in
--- clamav-1.4.5+dfsg/clamav-config.h.cmake.in 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/clamav-config.h.cmake.in 2026-08-06 23:12:04.000000000 +0200
@@ -98,6 +98,9 @@
/* use "Cache-Control: no-cache" in freshclam */
#cmakedefine FRESHCLAM_NO_CACHE 1
+/* Have funlinkat */
+#cmakedefine HAVE_FUNLINKAT 1
+
/* attrib aligned */
#cmakedefine HAVE_ATTRIB_ALIGNED 1
diff -Nru clamav-1.4.5+dfsg/clamd/thrmgr.c clamav-1.4.6+dfsg/clamd/thrmgr.c
--- clamav-1.4.5+dfsg/clamd/thrmgr.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/clamd/thrmgr.c 2026-08-06 23:12:04.000000000 +0200
@@ -28,6 +28,8 @@
#include <pthread.h>
#include <time.h>
#include <errno.h>
+#include <stdarg.h>
+#include <stdint.h>
#include <string.h>
// libclamav
@@ -118,6 +120,113 @@
} *pools = NULL;
static pthread_mutex_t pools_lock = PTHREAD_MUTEX_INITIALIZER;
+struct stats_buffer {
+ char *data;
+ size_t length;
+ size_t capacity;
+};
+
+struct queue_stats {
+ long min_wait;
+ long max_wait;
+ long total_wait;
+ unsigned valid;
+ unsigned invalid;
+ unsigned item_count;
+};
+
+/**
+ * @brief Free a buffered STATS response.
+ *
+ * @param buffer Response buffer to free.
+ */
+static void stats_buffer_cleanup(struct stats_buffer *buffer)
+{
+ if (!buffer)
+ return;
+
+ free(buffer->data);
+ memset(buffer, 0, sizeof(*buffer));
+}
+
+/**
+ * @brief Append formatted text to a buffered STATS response.
+ *
+ * @param buffer Response buffer to extend.
+ * @param format printf-style format string.
+ * @return CL_SUCCESS on success, or an error status.
+ */
+static cl_error_t stats_buffer_append(struct stats_buffer *buffer, const char *format, ...)
+{
+ cl_error_t status = CL_SUCCESS;
+ size_t required;
+ va_list args;
+ int needed;
+ int written;
+
+ va_start(args, format);
+ needed = vsnprintf(NULL, 0, format, args);
+ va_end(args);
+ if (needed < 0)
+ return CL_EFORMAT;
+
+ if ((size_t)needed >= SIZE_MAX - buffer->length)
+ return CL_EMEM;
+ required = buffer->length + (size_t)needed + 1;
+
+ if (required > buffer->capacity) {
+ size_t capacity = buffer->capacity ? buffer->capacity : 1024;
+ char *data;
+
+ while (capacity < required) {
+ if (capacity > SIZE_MAX / 2) {
+ capacity = required;
+ break;
+ }
+ capacity *= 2;
+ }
+
+ data = realloc(buffer->data, capacity);
+ if (!data)
+ return CL_EMEM;
+
+ buffer->data = data;
+ buffer->capacity = capacity;
+ }
+
+ va_start(args, format);
+ written = vsnprintf(buffer->data + buffer->length,
+ buffer->capacity - buffer->length, format, args);
+ va_end(args);
+ if (written != needed) {
+ status = CL_EFORMAT;
+ goto done;
+ }
+
+ buffer->length += (size_t)written;
+
+done:
+ return status;
+}
+
+/**
+ * @brief Free a task descriptor and its owned resources.
+ *
+ * The caller must ensure that the descriptor is no longer visible to STATS
+ * and that its worker thread has stopped using it.
+ *
+ * @param desc Task descriptor to free.
+ */
+static void task_desc_free(struct task_desc *desc)
+{
+ if (!desc)
+ return;
+
+ free(desc->filename);
+ pthread_mutex_destroy(&desc->mutex);
+ free(desc);
+}
+
static void add_topools(threadpool_t *t)
{
struct threadpool_list *new = malloc(sizeof(*new));
@@ -156,104 +265,238 @@
while (desc) {
struct task_desc *q = desc;
desc = desc->nxt;
- free(q);
+ task_desc_free(q);
}
t->tasks = NULL;
pthread_mutex_unlock(&pools_lock);
}
-static void print_queue(int f, work_queue_t *queue, struct timeval *tv_now)
+/**
+ * @brief Copy timing statistics from a work queue.
+ *
+ * The caller must hold the owning thread pool's mutex.
+ *
+ * @param queue Queue to summarize.
+ * @param tv_now Time used to calculate queue wait durations.
+ * @param stats Queue statistics to populate.
+ */
+static void stats_snapshot_queue(const work_queue_t *queue, const struct timeval *tv_now, struct queue_stats *stats)
{
- long umin = LONG_MAX, umax = 0, usum = 0;
- unsigned invalids = 0, cnt = 0;
- work_item_t *q;
+ const work_item_t *q;
+
+ memset(stats, 0, sizeof(*stats));
+ stats->min_wait = LONG_MAX;
+ stats->item_count = (unsigned)queue->item_count;
- if (!queue->head)
- return;
for (q = queue->head; q; q = q->next) {
long delta;
delta = tv_now->tv_usec - q->time_queued.tv_usec;
delta += (tv_now->tv_sec - q->time_queued.tv_sec) * 1000000;
if (delta < 0) {
- invalids++;
+ stats->invalid++;
continue;
}
- if (delta > umax)
- umax = delta;
- if (delta < umin)
- umin = delta;
- usum += delta;
- ++cnt;
- }
- mdprintf(f, " min_wait: %.6f max_wait: %.6f avg_wait: %.6f",
- umin / 1e6, umax / 1e6, usum / (1e6 * cnt));
- if (invalids)
- mdprintf(f, " (INVALID timestamps: %u)", invalids);
- if (cnt + invalids != (unsigned)queue->item_count)
- mdprintf(f, " (ERROR: %u != %u)", cnt + invalids,
- (unsigned)queue->item_count);
+ if (delta > stats->max_wait)
+ stats->max_wait = delta;
+ if (delta < stats->min_wait)
+ stats->min_wait = delta;
+ stats->total_wait += delta;
+ stats->valid++;
+ }
}
-int thrmgr_printstats(int f, char term)
+/**
+ * @brief Append a work queue snapshot to a buffered STATS response.
+ *
+ * @param buffer Response buffer to extend.
+ * @param stats Queue statistics to append.
+ * @return CL_SUCCESS on success, or an error status.
+ */
+static cl_error_t stats_buffer_append_queue(struct stats_buffer *buffer, const struct queue_stats *stats)
+{
+ cl_error_t status;
+
+ if (!stats->item_count)
+ return CL_SUCCESS;
+
+ status = stats_buffer_append(buffer, " min_wait: %.6f max_wait: %.6f avg_wait: %.6f",
+ stats->valid ? stats->min_wait / 1e6 : 0.0,
+ stats->valid ? stats->max_wait / 1e6 : 0.0,
+ stats->valid ? stats->total_wait / (1e6 * stats->valid) : 0.0);
+ if (CL_SUCCESS != status)
+ return status;
+
+ if (stats->invalid) {
+ status = stats_buffer_append(buffer, " (INVALID timestamps: %u)", stats->invalid);
+ if (CL_SUCCESS != status)
+ return status;
+ }
+
+ if (stats->valid + stats->invalid != stats->item_count) {
+ status = stats_buffer_append(buffer, " (ERROR: %u != %u)", stats->valid + stats->invalid,
+ stats->item_count);
+ if (CL_SUCCESS != status)
+ return status;
+ }
+
+ return CL_SUCCESS;
+}
+
+/**
+ * @brief Append a synchronized thread pool snapshot.
+ *
+ * The caller must hold pools_lock, which pins the pool while its mutex is
+ * acquired. No code may acquire pools_lock while holding a pool mutex.
+ *
+ * @param buffer Response buffer to extend.
+ * @param pool Thread pool to summarize.
+ * @param primary Whether this is the primary pool.
+ * @param tv_now Time captured for the subsequent task snapshot.
+ * @return CL_SUCCESS on success, or an error status.
+ */
+static cl_error_t stats_buffer_append_pool(struct stats_buffer *buffer, threadpool_t *pool, int primary, struct timeval *tv_now)
+{
+ struct queue_stats bulk_stats;
+ struct queue_stats single_stats;
+ cl_error_t status = CL_SUCCESS;
+ pool_state_t pool_state;
+ unsigned thr_alive;
+ unsigned thr_idle;
+ unsigned thr_max;
+ unsigned idle_timeout;
+ unsigned queue_items;
+ const char *state;
+
+ if (pthread_mutex_lock(&pool->pool_mutex) != 0) {
+ logg(LOGG_ERROR, "Unable to lock thread pool mutex for statistics\n");
+ return CL_ELOCK;
+ }
+
+ pool_state = pool->state;
+ thr_alive = (unsigned)pool->thr_alive;
+ thr_idle = (unsigned)pool->thr_idle;
+ thr_max = (unsigned)pool->thr_max;
+ idle_timeout = (unsigned)pool->idle_timeout;
+ queue_items = (unsigned)pool->single_queue->item_count + (unsigned)pool->bulk_queue->item_count;
+ gettimeofday(tv_now, NULL);
+ stats_snapshot_queue(pool->bulk_queue, tv_now, &bulk_stats);
+ stats_snapshot_queue(pool->single_queue, tv_now, &single_stats);
+
+ if (pthread_mutex_unlock(&pool->pool_mutex) != 0) {
+ logg(LOGG_ERROR, "Unable to unlock thread pool mutex for statistics\n");
+ return CL_ELOCK;
+ }
+
+ switch (pool_state) {
+ case POOL_INVALID:
+ state = "INVALID";
+ break;
+ case POOL_VALID:
+ state = "VALID";
+ break;
+ case POOL_EXIT:
+ state = "EXIT";
+ break;
+ default:
+ state = "??";
+ break;
+ }
+
+ status = stats_buffer_append(buffer, "STATE: %s %s\n", state, primary ? "PRIMARY" : "");
+ if (CL_SUCCESS != status)
+ goto done;
+
+ status = stats_buffer_append(buffer, "THREADS: live %u idle %u max %u idle-timeout %u\n",
+ thr_alive, thr_idle, thr_max, idle_timeout);
+ if (CL_SUCCESS != status)
+ goto done;
+
+ /* TODO: show both queues */
+ status = stats_buffer_append(buffer, "QUEUE: %u items", queue_items);
+ if (CL_SUCCESS != status)
+ goto done;
+
+ status = stats_buffer_append_queue(buffer, &bulk_stats);
+ if (CL_SUCCESS != status)
+ goto done;
+
+ status = stats_buffer_append_queue(buffer, &single_stats);
+ if (CL_SUCCESS != status)
+ goto done;
+
+ status = stats_buffer_append(buffer, "\n");
+
+done:
+ return status;
+}
+
+/**
+ * @brief Create an immutable snapshot of the STATS response.
+ *
+ * Pool and task state is formatted into an owned memory buffer while the
+ * corresponding descriptors are protected. The global pool lock is released
+ * before any response bytes are written to the client, so a slow or
+ * non-reading client cannot block scan-worker bookkeeping.
+ *
+ * @param response Response snapshot to populate.
+ * @return CL_SUCCESS on success, or an error status.
+ */
+static cl_error_t stats_response_create(struct stats_buffer *response)
{
struct threadpool_list *l;
+ cl_error_t status = CL_SUCCESS;
unsigned cnt, pool_cnt = 0;
- size_t pool_used = 0, pool_total = 0, seen_cnt = 0, error_flag = 0;
+ size_t pool_used = 0, pool_total = 0, seen_cnt = 0;
float mem_heap = 0, mem_mmap = 0, mem_used = 0, mem_free = 0, mem_releasable = 0;
- const struct cl_engine **seen = NULL;
- int has_libc_memstats = 0;
+ struct cl_engine **seen = NULL;
+ int has_libc_memstats = 0;
+
+ memset(response, 0, sizeof(*response));
+
+ if (pthread_mutex_lock(&pools_lock) != 0) {
+ logg(LOGG_ERROR, "Unable to lock thread pool statistics mutex\n");
+ return CL_ELOCK;
+ }
- pthread_mutex_lock(&pools_lock);
for (cnt = 0, l = pools; l; l = l->nxt) cnt++;
- mdprintf(f, "POOLS: %u\n\n", cnt);
- for (l = pools; l && !error_flag; l = l->nxt) {
+ status = stats_buffer_append(response, "POOLS: %u\n\n", cnt);
+ if (CL_SUCCESS != status)
+ goto unlock;
+
+ for (l = pools; l; l = l->nxt) {
threadpool_t *pool = l->pool;
- const char *state;
struct timeval tv_now;
struct task_desc *task;
- cnt = 0;
if (!pool) {
- mdprintf(f, "NULL\n\n");
+ status = stats_buffer_append(response, "NULL\n\n");
+ if (CL_SUCCESS != status)
+ goto unlock;
continue;
}
- /* now we can access desc->, knowing that they won't get freed
- * because the other tasks can't quit while pool_mutex is taken
- */
- switch (pool->state) {
- case POOL_INVALID:
- state = "INVALID";
- break;
- case POOL_VALID:
- state = "VALID";
- break;
- case POOL_EXIT:
- state = "EXIT";
- break;
- default:
- state = "??";
- break;
- }
- mdprintf(f, "STATE: %s %s\n", state, l->nxt ? "" : "PRIMARY");
- mdprintf(f, "THREADS: live %u idle %u max %u idle-timeout %u\n", pool->thr_alive, pool->thr_idle, pool->thr_max,
- pool->idle_timeout);
- /* TODO: show both queues */
- mdprintf(f, "QUEUE: %u items", pool->single_queue->item_count + pool->bulk_queue->item_count);
- gettimeofday(&tv_now, NULL);
- print_queue(f, pool->bulk_queue, &tv_now);
- print_queue(f, pool->single_queue, &tv_now);
- mdprintf(f, "\n");
+
+ status = stats_buffer_append_pool(response, pool, !l->nxt, &tv_now);
+ if (CL_SUCCESS != status)
+ goto unlock;
+
for (task = pool->tasks; task; task = task->nxt) {
double delta;
- size_t used, total;
+
+ if (pthread_mutex_lock(&task->mutex) != 0) {
+ logg(LOGG_ERROR, "Unable to lock task statistics mutex\n");
+ status = CL_ELOCK;
+ goto unlock;
+ }
delta = tv_now.tv_usec - task->tv.tv_usec;
delta += (tv_now.tv_sec - task->tv.tv_sec) * 1000000.0;
- mdprintf(f, "\t%s %f %s\n",
- task->command ? task->command : "N/A",
- delta / 1e6,
- task->filename ? task->filename : "");
- if (task->engine) {
+
+ status = stats_buffer_append(response, "\t%s %f %s\n",
+ task->command ? task->command : "N/A",
+ delta / 1e6,
+ task->filename ? task->filename : "");
+
+ if (CL_SUCCESS == status && task->engine) {
/* we usually have at most 2 engines so a linear
* search is good enough */
size_t i;
@@ -264,28 +507,59 @@
/* we need to count the memusage from the same
* engine only once */
if (i == seen_cnt) {
- const struct cl_engine **s;
+ struct cl_engine **s;
/* new engine */
- ++seen_cnt;
- s = realloc((void *)seen, seen_cnt * sizeof(*seen));
+ s = realloc(seen, (seen_cnt + 1) * sizeof(*seen));
if (!s) {
- error_flag = 1;
- break;
- }
- seen = s;
- seen[seen_cnt - 1] = task->engine;
-
- if (MPOOL_GETSTATS(task->engine, &used, &total) != -1) {
- pool_used += used;
- pool_total += total;
- pool_cnt++;
+ status = CL_EMEM;
+ } else {
+ seen = s;
+ status = cl_engine_addref((struct cl_engine *)task->engine);
+ if (CL_SUCCESS == status)
+ seen[seen_cnt++] = (struct cl_engine *)task->engine;
}
}
}
+
+ if (pthread_mutex_unlock(&task->mutex) != 0) {
+ logg(LOGG_ERROR, "Unable to unlock task statistics mutex\n");
+ status = CL_ELOCK;
+ }
+
+ if (CL_SUCCESS != status)
+ goto unlock;
}
- mdprintf(f, "\n");
+
+ status = stats_buffer_append(response, "\n");
+ if (CL_SUCCESS != status)
+ goto unlock;
}
- free((void *)seen);
+
+unlock:
+ if (pthread_mutex_unlock(&pools_lock) != 0) {
+ logg(LOGG_ERROR, "Unable to unlock thread pool statistics mutex\n");
+ status = CL_ELOCK;
+ }
+
+ if (CL_SUCCESS == status) {
+ for (cnt = 0; cnt < seen_cnt; cnt++) {
+ size_t used, total;
+
+ if (MPOOL_GETSTATS(seen[cnt], &used, &total) != -1) {
+ pool_used += used;
+ pool_total += total;
+ pool_cnt++;
+ }
+ }
+ }
+
+ for (cnt = 0; cnt < seen_cnt; cnt++)
+ cl_engine_free(seen[cnt]);
+ free(seen);
+
+ if (CL_SUCCESS != status)
+ goto done;
+
#ifdef HAVE_MALLINFO
{
struct mallinfo inf = mallinfo();
@@ -297,19 +571,35 @@
has_libc_memstats = 1;
}
#endif
- if (error_flag) {
- mdprintf(f, "ERROR: error encountered while formatting statistics\n");
+
+ if (has_libc_memstats)
+ status = stats_buffer_append(response,
+ "MEMSTATS: heap %.3fM mmap %.3fM used %.3fM free %.3fM releasable %.3fM pools %u pools_used %.3fM pools_total %.3fM\n",
+ mem_heap, mem_mmap, mem_used, mem_free, mem_releasable, pool_cnt,
+ pool_used / (1024 * 1024.0), pool_total / (1024 * 1024.0));
+ else
+ status = stats_buffer_append(response,
+ "MEMSTATS: heap N/A mmap N/A used N/A free N/A releasable N/A pools %u pools_used %.3fM pools_total %.3fM\n",
+ pool_cnt, pool_used / (1024 * 1024.0), pool_total / (1024 * 1024.0));
+
+done:
+ if (CL_SUCCESS != status)
+ stats_buffer_cleanup(response);
+
+ return status;
+}
+
+int thrmgr_printstats(int f, char term)
+{
+ struct stats_buffer response;
+
+ if (CL_SUCCESS == stats_response_create(&response)) {
+ mdprintf(f, "%sEND%c", response.data, term);
+ stats_buffer_cleanup(&response);
} else {
- if (has_libc_memstats)
- mdprintf(f, "MEMSTATS: heap %.3fM mmap %.3fM used %.3fM free %.3fM releasable %.3fM pools %u pools_used %.3fM pools_total %.3fM\n",
- mem_heap, mem_mmap, mem_used, mem_free, mem_releasable, pool_cnt,
- pool_used / (1024 * 1024.0), pool_total / (1024 * 1024.0));
- else
- mdprintf(f, "MEMSTATS: heap N/A mmap N/A used N/A free N/A releasable N/A pools %u pools_used %.3fM pools_total %.3fM\n",
- pool_cnt, pool_used / (1024 * 1024.0), pool_total / (1024 * 1024.0));
+ mdprintf(f, "ERROR: error encountered while formatting statistics\nEND%c", term);
}
- mdprintf(f, "END%c", term);
- pthread_mutex_unlock(&pools_lock);
+
return 0;
}
@@ -344,12 +634,13 @@
return;
}
}
- remove_frompools(threadpool);
if (pthread_mutex_unlock(&threadpool->pool_mutex) != 0) {
logg(LOGG_ERROR, "Mutex unlock failed\n");
exit(-1);
}
+ remove_frompools(threadpool);
+
pthread_mutex_destroy(&(threadpool->pool_mutex));
pthread_cond_destroy(&(threadpool->idle_cond));
pthread_cond_destroy(&(threadpool->queueable_single_cond));
@@ -540,21 +831,42 @@
static const char *IDLE_TASK = "IDLE";
-/* no mutex is needed, we are using thread local variable */
void thrmgr_setactivetask(const char *filename, const char *cmd)
{
struct task_desc *desc;
+ char *filename_copy = NULL;
+ char *old_filename;
+
pthread_once(&stats_tls_key_once, stats_tls_key_alloc);
desc = pthread_getspecific(stats_tls_key);
if (!desc)
return;
- desc->filename = filename;
+
+ if (filename) {
+ filename_copy = strdup(filename);
+ if (!filename_copy)
+ logg(LOGG_ERROR, "Unable to copy active task filename\n");
+ }
+
+ if (pthread_mutex_lock(&desc->mutex) != 0) {
+ logg(LOGG_ERROR, "Unable to lock task statistics mutex\n");
+ free(filename_copy);
+ return;
+ }
+
+ old_filename = desc->filename;
+ desc->filename = filename_copy;
if (cmd) {
- if (cmd == IDLE_TASK && desc->command == cmd)
- return;
- desc->command = cmd;
- gettimeofday(&desc->tv, NULL);
+ if (!(cmd == IDLE_TASK && desc->command == cmd)) {
+ desc->command = cmd;
+ gettimeofday(&desc->tv, NULL);
+ }
}
+
+ if (pthread_mutex_unlock(&desc->mutex) != 0)
+ logg(LOGG_ERROR, "Unable to unlock task statistics mutex\n");
+
+ free(old_filename);
}
void thrmgr_setactiveengine(const struct cl_engine *engine)
@@ -564,17 +876,40 @@
desc = pthread_getspecific(stats_tls_key);
if (!desc)
return;
+
+ if (pthread_mutex_lock(&desc->mutex) != 0) {
+ logg(LOGG_ERROR, "Unable to lock task statistics mutex\n");
+ return;
+ }
+
desc->engine = engine;
+
+ if (pthread_mutex_unlock(&desc->mutex) != 0)
+ logg(LOGG_ERROR, "Unable to unlock task statistics mutex\n");
}
-/* thread pool mutex must be held on entry */
+/* Must be called without pool_mutex to preserve the pools_lock -> pool_mutex
+ * lock order used when collecting statistics. */
static void stats_init(threadpool_t *pool)
{
struct task_desc *desc = calloc(1, sizeof(*desc));
if (!desc)
return;
+
+ if (pthread_mutex_init(&desc->mutex, NULL) != 0) {
+ logg(LOGG_ERROR, "Unable to initialize task statistics mutex\n");
+ free(desc);
+ return;
+ }
+
pthread_once(&stats_tls_key_once, stats_tls_key_alloc);
- pthread_setspecific(stats_tls_key, desc);
+ if (pthread_setspecific(stats_tls_key, desc) != 0) {
+ logg(LOGG_ERROR, "Unable to initialize task statistics state\n");
+ task_desc_free(desc);
+ return;
+ }
+
+ pthread_mutex_lock(&pools_lock);
if (!pool->tasks)
pool->tasks = desc;
else {
@@ -582,24 +917,34 @@
pool->tasks->prv = desc;
pool->tasks = desc;
}
+ pthread_mutex_unlock(&pools_lock);
}
-/* thread pool mutex must be held on entry */
-static void stats_destroy(threadpool_t *pool)
+/**
+ * @brief Unlink the calling worker's task descriptor.
+ *
+ * The caller must hold pools_lock. The returned descriptor is no longer
+ * visible to STATS and may be freed after releasing pools_lock.
+ *
+ * @param pool Thread pool owning the calling worker.
+ * @return The unlinked descriptor, or NULL if statistics were not initialized.
+ */
+static struct task_desc *stats_unlink_locked(threadpool_t *pool)
{
struct task_desc *desc = pthread_getspecific(stats_tls_key);
+
if (!desc)
- return;
- pthread_mutex_lock(&pools_lock);
+ return NULL;
+
if (desc->prv)
desc->prv->nxt = desc->nxt;
if (desc->nxt)
desc->nxt->prv = desc->prv;
if (pool->tasks == desc)
pool->tasks = desc->nxt;
- free(desc);
pthread_setspecific(stats_tls_key, NULL);
- pthread_mutex_unlock(&pools_lock);
+
+ return desc;
}
static inline int thrmgr_contended(threadpool_t *pool, int bulk)
@@ -661,20 +1006,21 @@
static void *thrmgr_worker(void *arg)
{
threadpool_t *threadpool = (threadpool_t *)arg;
+ struct task_desc *desc;
void *job_data;
- int retval, must_exit = FALSE, stats_inited = FALSE;
+ int retval, must_exit;
struct timespec timeout;
+ stats_init(threadpool);
+
/* loop looking for work */
for (;;) {
+ must_exit = FALSE;
+
if (pthread_mutex_lock(&(threadpool->pool_mutex)) != 0) {
logg(LOGG_ERROR, "Fatal: mutex lock failed\n");
exit(-2);
}
- if (!stats_inited) {
- stats_init(threadpool);
- stats_inited = TRUE;
- }
thrmgr_setactiveengine(NULL);
thrmgr_setactivetask(NULL, IDLE_TASK);
timeout.tv_sec = time(NULL) + threadpool->idle_timeout;
@@ -702,26 +1048,54 @@
if (job_data) {
threadpool->handler(job_data);
} else if (must_exit) {
- break;
+ /* A dispatcher can add work after the timed wait releases the
+ * pool mutex but before this worker retires. Make the final
+ * decision while holding both locks in the same order used by
+ * STATS so dispatch either observes this worker alive or starts
+ * a replacement after it has retired. */
+ if (pthread_mutex_lock(&pools_lock) != 0) {
+ logg(LOGG_ERROR, "Fatal: pools mutex lock failed\n");
+ exit(-2);
+ }
+ if (pthread_mutex_lock(&(threadpool->pool_mutex)) != 0) {
+ logg(LOGG_ERROR, "Fatal: mutex lock failed\n");
+ exit(-2);
+ }
+
+ if (threadpool->state == POOL_VALID &&
+ (threadpool->single_queue->item_count != 0 ||
+ threadpool->bulk_queue->item_count != 0)) {
+ if (pthread_mutex_unlock(&(threadpool->pool_mutex)) != 0) {
+ logg(LOGG_ERROR, "Fatal: mutex unlock failed\n");
+ exit(-2);
+ }
+ if (pthread_mutex_unlock(&pools_lock) != 0) {
+ logg(LOGG_ERROR, "Fatal: pools mutex unlock failed\n");
+ exit(-2);
+ }
+ continue;
+ }
+
+ desc = stats_unlink_locked(threadpool);
+ threadpool->thr_alive--;
+ if (threadpool->thr_alive == 0) {
+ /* signal that all threads are finished */
+ pthread_cond_broadcast(&threadpool->pool_cond);
+ }
+
+ if (pthread_mutex_unlock(&(threadpool->pool_mutex)) != 0) {
+ logg(LOGG_ERROR, "Fatal: mutex unlock failed\n");
+ exit(-2);
+ }
+ if (pthread_mutex_unlock(&pools_lock) != 0) {
+ logg(LOGG_ERROR, "Fatal: pools mutex unlock failed\n");
+ exit(-2);
+ }
+
+ task_desc_free(desc);
+ return NULL;
}
}
- if (pthread_mutex_lock(&(threadpool->pool_mutex)) != 0) {
- /* Fatal error */
- logg(LOGG_ERROR, "Fatal: mutex lock failed\n");
- exit(-2);
- }
- threadpool->thr_alive--;
- if (threadpool->thr_alive == 0) {
- /* signal that all threads are finished */
- pthread_cond_broadcast(&threadpool->pool_cond);
- }
- stats_destroy(threadpool);
- if (pthread_mutex_unlock(&(threadpool->pool_mutex)) != 0) {
- /* Fatal error */
- logg(LOGG_ERROR, "Fatal: mutex unlock failed\n");
- exit(-2);
- }
- return NULL;
}
static int thrmgr_dispatch_internal(threadpool_t *threadpool, void *user_data, int bulk)
diff -Nru clamav-1.4.5+dfsg/clamd/thrmgr.h clamav-1.4.6+dfsg/clamd/thrmgr.h
--- clamav-1.4.5+dfsg/clamd/thrmgr.h 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/clamd/thrmgr.h 2026-08-06 23:12:04.000000000 +0200
@@ -49,7 +49,8 @@
} pool_state_t;
struct task_desc {
- const char *filename;
+ pthread_mutex_t mutex;
+ char *filename;
const char *command;
struct timeval tv;
struct task_desc *prv;
diff -Nru clamav-1.4.5+dfsg/CMakeLists.txt clamav-1.4.6+dfsg/CMakeLists.txt
--- clamav-1.4.5+dfsg/CMakeLists.txt 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/CMakeLists.txt 2026-08-06 23:12:04.000000000 +0200
@@ -19,7 +19,7 @@
set(VERSION_SUFFIX "")
project( ClamAV
- VERSION "1.4.5"
+ VERSION "1.4.6"
DESCRIPTION "ClamAV open source email, web, and end-point anti-virus toolkit." )
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
@@ -809,6 +809,7 @@
set(HAVE_MKSTEMP 1)
set(HAVE_POLL 1)
else()
+ check_symbol_exists(funlinkat "unistd.h" HAVE_FUNLINKAT)
check_symbol_exists(fseeko "stdio.h" HAVE_FSEEKO)
check_symbol_exists(getaddrinfo "netdb.h" HAVE_GETADDRINFO)
check_symbol_exists(getpagesize "unistd.h" HAVE_GETPAGESIZE)
diff -Nru clamav-1.4.5+dfsg/common/actions.c clamav-1.4.6+dfsg/common/actions.c
--- clamav-1.4.5+dfsg/common/actions.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/common/actions.c 2026-08-06 23:12:04.000000000 +0200
@@ -437,6 +437,19 @@
return rc;
}
+#if defined(__FreeBSD__) && defined(HAVE_FUNLINKAT)
+static int action_funlinkat_nointr(int dirfd, const char *path, int fd, int flags)
+{
+ int rc;
+
+ do {
+ rc = funlinkat(dirfd, path, fd, flags);
+ } while ((rc < 0) && (EINTR == errno));
+
+ return rc;
+}
+#endif
+
#ifndef _WIN32
static int action_fstatat_nointr(int dirfd, const char *path, STATBUF *st, int flags)
{
@@ -2408,7 +2421,11 @@
return action_source_absolute_path_dup(path, action_path);
}
-static cl_error_t action_source_populate_posix(action_source_t *source, int fd, const char *open_path)
+static cl_error_t action_source_populate_posix(
+ action_source_t *source,
+ int fd,
+ const char *open_path,
+ bool open_path_is_resolved)
{
cl_error_t status = CL_EOPEN;
@@ -2423,6 +2440,20 @@
goto done;
}
+ if (open_path_is_resolved) {
+ /*
+ * Descriptor path lookup can return another name for the same file
+ * when it has multiple hard links. Preserve the path that was
+ * explicitly resolved and securely opened for path-based actions.
+ */
+ status = action_source_absolute_path_dup(open_path, &source->action_path);
+ if (CL_SUCCESS == status) {
+ cli_dbgmsg("action_source_populate_posix: Resolved action path for fd [%d] is: %s\n",
+ fd, source->action_path);
+ }
+ goto done;
+ }
+
status = cli_get_filepath_from_filedesc(fd, &source->action_path);
if (CL_SUCCESS != status) {
/*
@@ -2556,7 +2587,7 @@
goto done;
}
- status = action_source_populate_posix(source, fd, open_path);
+ status = action_source_populate_posix(source, fd, open_path, require_resolved_path);
if (CL_SUCCESS != status) {
goto done;
}
@@ -2637,7 +2668,7 @@
goto done;
}
- status = action_source_populate_posix(source, dup_fd, display_path);
+ status = action_source_populate_posix(source, dup_fd, display_path, false);
if (CL_SUCCESS != status) {
goto done;
}
@@ -3313,12 +3344,16 @@
* This approach mitigates the possibility that one of the directories
* in the path has been replaced with a malicious symlink.
*
- * @param target A file to be deleted.
- * @return 0 Unlink succeeded.
- * @return -1 Unlink failed.
+ * @param target A file to be deleted.
+ * @param source_fd POSIX descriptor for the scanned source.
+ * @param expected_stat POSIX metadata for the scanned source.
+ * @param target_file_handle Windows handle for the scanned source.
+ * @param target_file_handle_can_delete Whether the Windows handle has delete access.
+ * @return 0 Unlink succeeded.
+ * @return -1 Unlink failed.
*/
#ifndef _WIN32
-static int traverse_unlink(const char *target, const STATBUF *expected_stat)
+static int traverse_unlink(const char *target, int source_fd, const STATBUF *expected_stat)
#else
static int traverse_unlink(
const char *target,
@@ -3347,6 +3382,10 @@
goto done;
}
+#if !defined(_WIN32) && (!defined(__FreeBSD__) || !defined(HAVE_FUNLINKAT))
+ UNUSEDPARAM(source_fd);
+#endif
+
#ifndef _WIN32
/* On posix, we want a file descriptor for the directory */
if (0 != traverse_to(target, true, &target_directory_fd)) {
@@ -3382,6 +3421,30 @@
goto done;
}
} else {
+#if defined(__FreeBSD__) && defined(HAVE_FUNLINKAT)
+ if (source_fd >= 0) {
+ /*
+ * FreeBSD verifies atomically that target_basename still names
+ * source_fd before unlinking it. This avoids the private capture
+ * and no-replace restore required on other POSIX platforms.
+ */
+ if (0 != action_funlinkat_nointr(target_directory_fd, target_basename, source_fd, 0)) {
+ int unlink_errno = errno;
+
+ if (EDEADLK == unlink_errno) {
+ logg(LOGG_INFO, "traverse_unlink: Refusing to unlink '%s' because the source changed after validation.\n", target);
+ errno = EAGAIN;
+ } else {
+ logg(LOGG_INFO, "traverse_unlink: Failed to unlink '%s' through its opened descriptor: %s\n", target, strerror(unlink_errno));
+ errno = unlink_errno;
+ }
+ goto done;
+ }
+
+ status = 0;
+ goto done;
+ }
+#endif
if (0 != action_create_private_unlink_dir(
target_directory_fd,
private_directory_name,
@@ -3541,7 +3604,7 @@
notmoved++;
goto done;
}
- if (0 != traverse_unlink(action_filename, &source_stat)) {
+ if (0 != traverse_unlink(action_filename, source->scan_fd, &source_stat)) {
int unlink_errno = errno;
if (show_action_path) {
logg(LOGG_ERROR, "Can't unlink '%s' (real path: '%s') after linking into quarantine: %s\n", filename, action_filename, strerror(unlink_errno));
@@ -3613,7 +3676,7 @@
goto done;
}
#ifndef _WIN32
- if (0 != traverse_unlink(action_filename, &source_stat)) {
+ if (0 != traverse_unlink(action_filename, source->scan_fd, &source_stat)) {
if (show_action_path) {
logg(LOGG_ERROR, "Can't unlink '%s' (real path: '%s') after copy: %s\n", filename, action_filename, strerror(errno));
} else {
@@ -3742,7 +3805,7 @@
#ifndef _WIN32
if ((false == source->has_stat) ||
!S_ISREG(source->statbuf.st_mode) ||
- (0 != traverse_unlink(action_filename, &source->statbuf))) {
+ (0 != traverse_unlink(action_filename, source->scan_fd, &source->statbuf))) {
#else
if (0 != traverse_unlink(
action_filename,
diff -Nru clamav-1.4.5+dfsg/common/output.c clamav-1.4.6+dfsg/common/output.c
--- clamav-1.4.5+dfsg/common/output.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/common/output.c 2026-08-06 23:12:04.000000000 +0200
@@ -170,7 +170,7 @@
pthread_mutex_lock(&mdprintf_mutex);
#endif
while (todo > 0) {
- ret = send(desc, buff, bytes, 0);
+ ret = send(desc, buff, todo, 0);
if (ret < 0) {
struct timeval tv;
if (errno != EWOULDBLOCK)
diff -Nru clamav-1.4.5+dfsg/debian/changelog clamav-1.4.6+dfsg/debian/changelog
--- clamav-1.4.5+dfsg/debian/changelog 2026-07-11 15:42:12.000000000 +0200
+++ clamav-1.4.6+dfsg/debian/changelog 2026-08-17 16:22:11.000000000 +0200
@@ -1,3 +1,22 @@
+clamav (1.4.6+dfsg-1) unstable; urgency=high
+
+ * Import 1.4.6 (Closes: #1143939)
+ - CVE-2026-20345 ("Fixed an indexing error while converting GPT
+ partition names")
+ - CVE-2026-20339 ("Fixed an integer overflow in the PESpin unpacker")
+ - CVE-2026-20346 ("Fixed an integer underflow in the PDF parser")
+ - CVE-2026-20347 ("Fixed undefined behavior and integer overflow in the
+ Mach-O parser")
+ - CVE-2026-20348 ("Fixed XAR parser size handling")
+ - Fixed thread-safety issues in the clamd STATS command that could
+ disclose process memory or crash the daemon
+ * Refresh 0004-unit_tests-Don-t-set-TMP.patch
+ * Update symbols and the CL_FLEVEL guard in d/rules for 1.4.6
+ (CL_FLEVEL 215 -> 216)
+ * d/ci: Disable blhc (unreleased change already in the packaging repository)
+
+ -- Edmund Lodewijks <edmund at proteamail.com> Mon, 17 Aug 2026 16:22:11 +0200
+
clamav (1.4.5+dfsg-3) unstable; urgency=medium
* Allow the testsuite in parallel
diff -Nru clamav-1.4.5+dfsg/debian/libclamav12.symbols clamav-1.4.6+dfsg/debian/libclamav12.symbols
--- clamav-1.4.5+dfsg/debian/libclamav12.symbols 2026-07-11 15:27:37.000000000 +0200
+++ clamav-1.4.6+dfsg/debian/libclamav12.symbols 2026-08-17 16:22:11.000000000 +0200
@@ -3,10 +3,10 @@
*@CLAMAV_0.104.0 1.2.1
*@CLAMAV_1.0.0 1.2.1
*@CLAMAV_1.1.0 1.2.1
- *@CLAMAV_PRIVATE 1.4.5
+ *@CLAMAV_PRIVATE 1.4.6
*@CLAMAV_PUBLIC 1.2.1
- cl_retflevel at CLAMAV_PUBLIC 1.4.5
+ cl_retflevel at CLAMAV_PUBLIC 1.4.6
libfreshclam.so.3 libclamav12 #MINVER#
* Build-Depends-Package: libclamav-dev
- *@FRESHCLAM_PRIVATE 1.4.5
- *@FRESHCLAM_PUBLIC 1.4.5
+ *@FRESHCLAM_PRIVATE 1.4.6
+ *@FRESHCLAM_PUBLIC 1.4.6
diff -Nru clamav-1.4.5+dfsg/debian/patches/0004-unit_tests-Don-t-set-TMP.patch clamav-1.4.6+dfsg/debian/patches/0004-unit_tests-Don-t-set-TMP.patch
--- clamav-1.4.5+dfsg/debian/patches/0004-unit_tests-Don-t-set-TMP.patch 2026-07-11 15:27:38.000000000 +0200
+++ clamav-1.4.6+dfsg/debian/patches/0004-unit_tests-Don-t-set-TMP.patch 2026-08-17 16:04:22.000000000 +0200
@@ -16,11 +16,11 @@
unit_tests/CMakeLists.txt | 1 -
1 file changed, 1 deletion(-)
-diff --git a/unit_tests/CMakeLists.txt b/unit_tests/CMakeLists.txt
-index bf7e2ef..a4d2ea4 100644
---- a/unit_tests/CMakeLists.txt
-+++ b/unit_tests/CMakeLists.txt
-@@ -205,7 +205,6 @@ else()
+Index: clamav-1.4.6+dfsg/unit_tests/CMakeLists.txt
+===================================================================
+--- clamav-1.4.6+dfsg.orig/unit_tests/CMakeLists.txt
++++ clamav-1.4.6+dfsg/unit_tests/CMakeLists.txt
+@@ -208,7 +208,6 @@ else()
set(SOURCE ${CMAKE_SOURCE_DIR})
set(BUILD ${CMAKE_BINARY_DIR})
diff -Nru clamav-1.4.5+dfsg/debian/rules clamav-1.4.6+dfsg/debian/rules
--- clamav-1.4.5+dfsg/debian/rules 2026-07-11 15:41:16.000000000 +0200
+++ clamav-1.4.6+dfsg/debian/rules 2026-08-17 16:22:11.000000000 +0200
@@ -99,7 +99,7 @@
fi;\
done; \
# Check for library features which may have been upgraded.
- if ! grep -q "CL_FLEVEL 215" libclamav/others.h ; then \
+ if ! grep -q "CL_FLEVEL 216" libclamav/others.h ; then \
echo "cl_retflevel needs boosting in symbol file"; \
touch debian/exit; \
fi;
diff -Nru clamav-1.4.5+dfsg/debian/salsa-ci.yml clamav-1.4.6+dfsg/debian/salsa-ci.yml
--- clamav-1.4.5+dfsg/debian/salsa-ci.yml 2026-07-11 15:27:37.000000000 +0200
+++ clamav-1.4.6+dfsg/debian/salsa-ci.yml 2026-08-17 10:08:52.000000000 +0200
@@ -1,3 +1,7 @@
---
include:
- https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml
+
+variables:
+ # Some RUST lines are identified as "NONVERBOSE BUILD"
+ SALSA_CI_DISABLE_BLHC: 1
diff -Nru clamav-1.4.5+dfsg/.gitattributes clamav-1.4.6+dfsg/.gitattributes
--- clamav-1.4.5+dfsg/.gitattributes 1970-01-01 02:00:00.000000000 +0200
+++ clamav-1.4.6+dfsg/.gitattributes 2026-08-06 23:12:04.000000000 +0200
@@ -0,0 +1,19 @@
+# Handle line endings automatically for files detected as text
+# and leave all files detected as binary untouched.
+* text=auto
+
+#
+# The above will handle all files NOT found below
+#
+
+# Files that should be left untouched (binary is macro for -text -diff)
+*.ref binary
+
+# Preserve signature for .cargo/vendor files (from the tarabll)
++/.cargo/vendor binary
+
+#
+# Exclude files from exporting
+#
+.gitattributes export-ignore
+.gitignore export-ignore
diff -Nru clamav-1.4.5+dfsg/Jenkinsfile clamav-1.4.6+dfsg/Jenkinsfile
--- clamav-1.4.5+dfsg/Jenkinsfile 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/Jenkinsfile 2026-08-06 23:12:04.000000000 +0200
@@ -10,7 +10,7 @@
parameters(
[
string(name: 'VERSION',
- defaultValue: '1.4.5',
+ defaultValue: '1.4.6',
description: 'ClamAV version string'),
string(name: 'FRAMEWORK_BRANCH',
defaultValue: '1.4',
@@ -49,7 +49,7 @@
defaultValue: '1.4',
description: 'private-fuzz-corpus branch'),
string(name: 'SHARED_LIB_BRANCH',
- defaultValue: 'master',
+ defaultValue: 'main',
description: 'tests-jenkins-shared-libraries branch')
]
)
diff -Nru clamav-1.4.5+dfsg/libclamav/bytecode_api.h clamav-1.4.6+dfsg/libclamav/bytecode_api.h
--- clamav-1.4.5+dfsg/libclamav/bytecode_api.h 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/bytecode_api.h 2026-08-06 23:12:04.000000000 +0200
@@ -198,6 +198,7 @@
FUNC_LEVEL_1_4_3 = 213, /**< LibClamAV release 1.4.3 */
FUNC_LEVEL_1_4_4 = 214, /**< LibClamAV release 1.4.4 */
FUNC_LEVEL_1_4_5 = 215, /**< LibClamAV release 1.4.5 */
+ FUNC_LEVEL_1_4_6 = 216, /**< LibClamAV release 1.4.6 */
};
/**
diff -Nru clamav-1.4.5+dfsg/libclamav/gpt.c clamav-1.4.6+dfsg/libclamav/gpt.c
--- clamav-1.4.5+dfsg/libclamav/gpt.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/gpt.c 2026-08-06 23:12:04.000000000 +0200
@@ -303,7 +303,7 @@
gpe.lastLBA = le64_to_host(gpe.lastLBA);
gpe.attributes = le64_to_host(gpe.attributes);
for (j = 0; j < 36; ++j) {
- gpe.name[i] = le16_to_host(gpe.name[i]);
+ gpe.name[j] = le16_to_host(gpe.name[j]);
}
/* check that partition is not empty and within a valid location */
diff -Nru clamav-1.4.5+dfsg/libclamav/macho.c clamav-1.4.6+dfsg/libclamav/macho.c
--- clamav-1.4.5+dfsg/libclamav/macho.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/macho.c 2026-08-06 23:12:04.000000000 +0200
@@ -46,6 +46,11 @@
#define EC32(v, conv) (conv ? cbswap32(v) : v)
#define EC64(v, conv) (conv ? cbswap64(v) : v)
+#define MACHO_SECTION_TYPE_MASK 0x000000ff
+#define MACHO_S_ZEROFILL 0x1
+#define MACHO_S_GB_ZEROFILL 0xc
+#define MACHO_S_THREAD_LOCAL_ZEROFILL 0x12
+
struct macho_hdr {
uint32_t magic;
uint32_t cpu_type;
@@ -195,6 +200,52 @@
return vaddr - sects[i].rva + sects[i].raw;
}
+/**
+ * Calculate the raw section size implied by a Mach-O alignment exponent.
+ *
+ * Mach-O section alignment is encoded as log2(bytes). Reject malformed
+ * exponents and rounded sizes that cannot fit in cli_exe_section.rsz.
+ * Zero-fill sections do not occupy file bytes, so they have no raw size.
+ */
+static bool cli_macho_section_raw_size(uint64_t virtual_size,
+ uint32_t align_exponent,
+ uint32_t section_flags,
+ uint32_t *raw_size)
+{
+ uint64_t alignment;
+ uint64_t remainder;
+ uint64_t padding;
+ uint64_t rounded_size;
+ uint32_t section_type = section_flags & MACHO_SECTION_TYPE_MASK;
+
+ if (align_exponent > 31) {
+ return false;
+ }
+
+ if (section_type == MACHO_S_ZEROFILL ||
+ section_type == MACHO_S_GB_ZEROFILL ||
+ section_type == MACHO_S_THREAD_LOCAL_ZEROFILL) {
+ *raw_size = 0;
+ return true;
+ }
+
+ if (virtual_size > UINT32_MAX) {
+ return false;
+ }
+
+ alignment = (uint64_t)1 << align_exponent;
+ remainder = virtual_size % alignment;
+ padding = (alignment - remainder) % alignment;
+ rounded_size = virtual_size + padding;
+
+ if (rounded_size > UINT32_MAX) {
+ return false;
+ }
+
+ *raw_size = (uint32_t)rounded_size;
+ return true;
+}
+
cl_error_t cli_scanmacho(cli_ctx *ctx, struct cli_exe_info *fileinfo)
{
struct macho_hdr hdr;
@@ -383,17 +434,26 @@
for (j = 0; j < nsects; j++) {
if (m64) {
+ uint64_t section_size;
+
if (fmap_readn(map, §ion64, at, sizeof(section64)) != sizeof(section64)) {
cli_dbgmsg("cli_scanmacho: Can't read section\n");
free(sections);
RETURN_BROKEN;
}
at += sizeof(section64);
+ section_size = EC64(section64.size, conv);
sections[sect].rva = EC64(section64.addr, conv);
- sections[sect].vsz = EC64(section64.size, conv);
sections[sect].raw = EC32(section64.offset, conv);
- section64.align = 1 << EC32(section64.align, conv);
- sections[sect].rsz = sections[sect].vsz + (section64.align - (sections[sect].vsz % section64.align)) % section64.align; /* most likely we can assume it's the same as .vsz */
+ if (!cli_macho_section_raw_size(section_size,
+ EC32(section64.align, conv),
+ EC32(section64.flags, conv),
+ §ions[sect].rsz)) {
+ cli_dbgmsg("cli_scanmacho: Section alignment or size is malformed\n");
+ free(sections);
+ RETURN_BROKEN;
+ }
+ sections[sect].vsz = (uint32_t)section_size;
strncpy(name, section64.sectname, sizeof(name));
name[sizeof(name) - 1] = '\0';
} else {
@@ -406,13 +466,14 @@
sections[sect].rva = EC32(section.addr, conv);
sections[sect].vsz = EC32(section.size, conv);
sections[sect].raw = EC32(section.offset, conv);
- if (EC32(section.align, conv) >= 32) {
- cli_dbgmsg("cli_scanmacho: Section aligned is malformed\n");
+ if (!cli_macho_section_raw_size(sections[sect].vsz,
+ EC32(section.align, conv),
+ EC32(section.flags, conv),
+ §ions[sect].rsz)) {
+ cli_dbgmsg("cli_scanmacho: Section alignment or size is malformed\n");
free(sections);
RETURN_BROKEN;
}
- section.align = 1 << EC32(section.align, conv);
- sections[sect].rsz = sections[sect].vsz + (section.align - (sections[sect].vsz % section.align)) % section.align;
strncpy(name, section.sectname, sizeof(name));
name[sizeof(name) - 1] = '\0';
}
diff -Nru clamav-1.4.5+dfsg/libclamav/mpool.c clamav-1.4.6+dfsg/libclamav/mpool.c
--- clamav-1.4.5+dfsg/libclamav/mpool.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/mpool.c 2026-08-06 23:12:04.000000000 +0200
@@ -562,8 +562,8 @@
const struct MPMAP *mpm;
const mpool_t *mp;
- /* checking refcount is not necessary, but safer */
- if (!eng || !eng->refcount)
+ /* The caller must retain a reference while statistics are collected. */
+ if (!eng)
return -1;
mp = eng->mempool;
if (!mp)
diff -Nru clamav-1.4.5+dfsg/libclamav/mpool.h clamav-1.4.6+dfsg/libclamav/mpool.h
--- clamav-1.4.5+dfsg/libclamav/mpool.h 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/mpool.h 2026-08-06 23:12:04.000000000 +0200
@@ -47,6 +47,13 @@
char *cli_mpool_virname(mpool_t *mpool, const char *virname, unsigned int official);
uint16_t *cli_mpool_hex2ui(mpool_t *mpool, const char *hex);
void mpool_flush(mpool_t *mpool);
+
+/**
+ * @brief Get memory usage statistics for an engine's memory pool.
+ *
+ * The caller must retain a reference to the engine for the duration of the
+ * call.
+ */
int mpool_getstats(const struct cl_engine *engine, size_t *used, size_t *total);
#define MPOOL_MALLOC(a, b) mpool_malloc(a, b)
diff -Nru clamav-1.4.5+dfsg/libclamav/others_common.c clamav-1.4.6+dfsg/libclamav/others_common.c
--- clamav-1.4.5+dfsg/libclamav/others_common.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/others_common.c 2026-08-06 23:12:04.000000000 +0200
@@ -41,6 +41,9 @@
#endif
#include <time.h>
#include <fcntl.h>
+#ifdef __FreeBSD__
+#include <sys/user.h>
+#endif
#ifdef HAVE_PWD_H
#include <pwd.h>
#endif
@@ -1489,6 +1492,31 @@
if (NULL == evaluated_filepath) {
cli_errmsg("cli_get_filepath_from_filedesc: Failed to allocate memory to store filename\n");
status = CL_EMEM;
+ goto done;
+ }
+
+#elif defined(__FreeBSD__) && defined(F_KINFO)
+
+ struct kinfo_file file_info;
+
+ if (NULL == filepath) {
+ cli_errmsg("cli_get_filepath_from_filedesc: Invalid args.\n");
+ goto done;
+ }
+
+ memset(&file_info, 0, sizeof(file_info));
+ file_info.kf_structsize = sizeof(file_info);
+
+ if ((fcntl(desc, F_KINFO, &file_info) < 0) || ('\0' == file_info.kf_path[0])) {
+ cli_dbgmsg("cli_get_filepath_from_filedesc: Failed to resolve filename for descriptor %d\n", desc);
+ status = CL_EOPEN;
+ goto done;
+ }
+
+ evaluated_filepath = CLI_STRNDUP(file_info.kf_path, CLI_STRNLEN(file_info.kf_path, sizeof(file_info.kf_path)));
+ if (NULL == evaluated_filepath) {
+ cli_errmsg("cli_get_filepath_from_filedesc: Failed to allocate memory to store filename\n");
+ status = CL_EMEM;
goto done;
}
diff -Nru clamav-1.4.5+dfsg/libclamav/others.h clamav-1.4.6+dfsg/libclamav/others.h
--- clamav-1.4.5+dfsg/libclamav/others.h 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/others.h 2026-08-06 23:12:04.000000000 +0200
@@ -69,7 +69,7 @@
* in re-enabling affected modules.
*/
-#define CL_FLEVEL 215
+#define CL_FLEVEL 216
#define CL_FLEVEL_DCONF CL_FLEVEL
#define CL_FLEVEL_SIGTOOL CL_FLEVEL
@@ -1255,7 +1255,7 @@
* @brief Attempt to get a filename from an open file descriptor.
*
* Caller is responsible for free'ing the filename.
- * Should work on Linux, macOS, Windows.
+ * Should work on Linux, FreeBSD when F_KINFO is available, macOS, Windows.
*
* @param desc File descriptor
* @param[out] filepath Will be set to file path if found, or NULL.
diff -Nru clamav-1.4.5+dfsg/libclamav/pdf.c clamav-1.4.6+dfsg/libclamav/pdf.c
--- clamav-1.4.5+dfsg/libclamav/pdf.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/pdf.c 2026-08-06 23:12:04.000000000 +0200
@@ -2680,6 +2680,8 @@
start = ++q;
len -= 1;
}
+ if (len <= 0)
+ return NULL;
q = memchr(q + 1, '>', len - 1);
if (!q)
return NULL;
diff -Nru clamav-1.4.5+dfsg/libclamav/spin.c clamav-1.4.6+dfsg/libclamav/spin.c
--- clamav-1.4.5+dfsg/libclamav/spin.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/spin.c 2026-08-06 23:12:04.000000000 +0200
@@ -62,6 +62,64 @@
#include "packlibs.h"
#include "spin.h"
+/**
+ * @brief Add a section size to the rebuilt PESpin image size.
+ *
+ * @return 0 if the size was added, 1 if the rebuilt image would exceed
+ * CLI_MAX_ALLOCATION.
+ */
+static int spin_add_blob_size(uint64_t *blobsz, uint32_t size)
+{
+ if (*blobsz > (uint64_t)CLI_MAX_ALLOCATION ||
+ (uint64_t)size > (uint64_t)CLI_MAX_ALLOCATION - *blobsz) {
+ cli_dbgmsg("spin: rebuilt section data exceeds allocation limit\n");
+ return 1;
+ }
+
+ *blobsz += size;
+ return 0;
+}
+
+/**
+ * @brief Replace an already-counted section size in the rebuilt PESpin image.
+ *
+ * @return 0 if the size was replaced, 1 if the previous size was not counted
+ * or the rebuilt image would exceed CLI_MAX_ALLOCATION.
+ */
+static int spin_replace_blob_size(uint64_t *blobsz, uint32_t old_size, uint32_t new_size)
+{
+ uint64_t adjusted_blobsz;
+
+ if (*blobsz < old_size) {
+ cli_dbgmsg("spin: rebuilt section data accounting underflow\n");
+ return 1;
+ }
+
+ adjusted_blobsz = *blobsz - old_size;
+ if (adjusted_blobsz > (uint64_t)CLI_MAX_ALLOCATION ||
+ (uint64_t)new_size > (uint64_t)CLI_MAX_ALLOCATION - adjusted_blobsz) {
+ cli_dbgmsg("spin: rebuilt section data exceeds allocation limit\n");
+ return 1;
+ }
+
+ *blobsz = adjusted_blobsz + new_size;
+ return 0;
+}
+
+/**
+ * @brief Free PESpin section buffers that are marked as grown in the bitmap.
+ */
+static void spin_free_grown_sections(char **sects, int sectcnt, uint32_t bitmap)
+{
+ int j;
+
+ for (j = 0; j < sectcnt; j++) {
+ if (bitmap & 1)
+ free(sects[j]);
+ bitmap >>= 1;
+ }
+}
+
static char exec86(uint8_t aelle, uint8_t cielle, char *curremu, int *retval)
{
int len = 0;
@@ -143,7 +201,7 @@
int i;
while (size) {
- eax ^= *src++ << 8 & 0xff00;
+ eax ^= ((uint32_t)(uint8_t)*src++ << 8) & 0xff00;
eax = eax >> 3 & 0x1fffffff;
for (i = 0; i < 4; i++) {
uint32_t swap;
@@ -164,7 +222,8 @@
{
char *curr, *emu, *ep, *spinned;
char **sects;
- int blobsz = 0, j;
+ uint64_t blobsz = 0;
+ int j;
uint32_t key32, bitmap, bitman;
uint32_t len;
uint8_t key8;
@@ -402,12 +461,16 @@
len = 0;
for (j = 0; j < sectcnt; j++) {
if (bitmap & 1) {
+ if (spin_add_blob_size(&blobsz, sections[j].vsz)) {
+ len = 1;
+ break;
+ }
+
if ((sects[j] = (char *)cli_max_malloc(sections[j].vsz)) == NULL) {
cli_dbgmsg("spin: malloc(%u) failed\n", sections[j].vsz);
len = 1;
break;
}
- blobsz += sections[j].vsz;
memset(sects[j], 0, sections[j].vsz);
cli_dbgmsg("spin: Growing sect%d: was %x will be %x\n", j, sections[j].rsz, sections[j].vsz);
if (cli_unfsg(src + sections[j].raw, sects[j], sections[j].rsz, sections[j].vsz, NULL, NULL) == -1) {
@@ -415,7 +478,11 @@
cli_dbgmsg("spin: Unpack failure\n");
}
} else {
- blobsz += sections[j].rsz;
+ if (spin_add_blob_size(&blobsz, sections[j].rsz)) {
+ len = 1;
+ break;
+ }
+
sects[j] = src + sections[j].raw;
cli_dbgmsg("spin: Not growing sect%d\n", j);
}
@@ -425,12 +492,7 @@
cli_dbgmsg("spin: decompression complete\n");
if (len) {
- int t;
- for (t = 0; t < j; t++) {
- if (bitman & 1)
- free(sects[t]);
- bitman = bitman >> 1 & 0x7fffffff;
- }
+ spin_free_grown_sections(sects, j, bitman);
free(sects);
return 1;
}
@@ -444,7 +506,7 @@
break;
}
- if (j != sectcnt && ((bitman & (1 << j)) == 0)) { /* FIXME: not really sure either the res sect is lamed or just compressed, but this'll save some major headaches */
+ if (j != sectcnt && j < 32 && ((bitman & ((uint32_t)1 << j)) == 0)) { /* FIXME: not really sure either the res sect is lamed or just compressed, but this'll save some major headaches */
cli_dbgmsg("spin: Resources (sect%d) appear to be compressed\n\tuncompressed offset %x, len %x\n\tcompressed offset %x, len %x\n", j, sections[j].rva, key32 - sections[j].rva, key32, sections[j].vsz - (key32 - sections[j].rva));
if ((curr = (char *)cli_max_malloc(sections[j].vsz)) != NULL) {
@@ -454,26 +516,34 @@
free(curr);
cli_dbgmsg("spin: Failed to grow resources, continuing anyway\n");
- blobsz += sections[j].rsz;
} else {
- sects[j] = curr;
- bitman |= 1 << j;
- cli_dbgmsg("spin: Resources grown\n");
- blobsz += sections[j].vsz;
+ if (spin_replace_blob_size(&blobsz, sections[j].rsz, sections[j].vsz)) {
+ free(curr);
+ len = 1;
+ } else {
+ sects[j] = curr;
+ bitman |= (uint32_t)1 << j;
+ cli_dbgmsg("spin: Resources grown\n");
+ }
}
} else {
/* malloc failed but i'm too deep into this crap to quit without leaking more :( */
cli_dbgmsg("spin: memory allocation failed, continuing anyway\n");
- blobsz += sections[j].rsz;
}
} else {
cli_dbgmsg("spin: No res?!\n");
}
}
+ if (len) {
+ spin_free_grown_sections(sects, sectcnt, bitman);
+ free(sects);
+ return 1;
+ }
+
bitmap = bitman; /* save as a free() bitmap */
- if ((ep = (char *)cli_max_malloc(blobsz)) != NULL) {
+ if ((ep = (char *)cli_max_malloc((size_t)blobsz)) != NULL) {
struct cli_exe_section *rebhlp;
if ((rebhlp = (struct cli_exe_section *)cli_max_malloc(sizeof(struct cli_exe_section) * (sectcnt))) != NULL) {
char *to = ep;
@@ -505,11 +575,7 @@
}
cli_dbgmsg("spin: free bitmap is %x\n", bitman);
- for (j = 0; j < sectcnt; j++) {
- if (bitmap & 1)
- free(sects[j]);
- bitmap = bitmap >> 1 & 0x7fffffff;
- }
+ spin_free_grown_sections(sects, sectcnt, bitmap);
free(sects);
return 1; /* :( */
}
diff -Nru clamav-1.4.5+dfsg/libclamav/xar.c clamav-1.4.6+dfsg/libclamav/xar.c
--- clamav-1.4.5+dfsg/libclamav/xar.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/libclamav/xar.c 2026-08-06 23:12:04.000000000 +0200
@@ -34,6 +34,8 @@
#include "inflate64.h"
#include "lzma_iface.h"
+#define XAR_TOC_INFLATE_CHUNK_SIZE (64 * 1024)
+
/*
xar_cleanup_temp_file - cleanup after cli_gentempfd
parameters:
@@ -408,6 +410,124 @@
return memcmp(result, expected, len);
}
+static cl_error_t xar_inflate_toc(cli_ctx *ctx, const unsigned char *compressed_toc, size_t compressed_length,
+ char **toc_out, size_t *toc_length_out)
+{
+ cl_error_t ret = CL_SUCCESS;
+ z_stream strm;
+ char *toc = NULL;
+ size_t capacity = XAR_TOC_INFLATE_CHUNK_SIZE;
+ size_t toc_length = 0;
+ bool inflate_initialized = false;
+
+ memset(&strm, 0, sizeof(strm));
+
+ if (compressed_length > UINT_MAX) {
+ cli_dbgmsg("cli_scanxar: Compressed TOC is too large for zlib.\n");
+ return CL_EFORMAT;
+ }
+
+ toc = cli_max_malloc(capacity);
+ if (toc == NULL) {
+ cli_dbgmsg("cli_scanxar: Failed to allocate the initial TOC buffer.\n");
+ return CL_EMEM;
+ }
+
+ strm.next_in = (unsigned char *)compressed_toc;
+ strm.avail_in = (uInt)compressed_length;
+
+ if (inflateInit(&strm) != Z_OK) {
+ cli_dbgmsg("cli_scanxar: inflateInit failed.\n");
+ ret = CL_EFORMAT;
+ goto done;
+ }
+ inflate_initialized = true;
+
+ while (true) {
+ uInt avail_in_before;
+ uInt output_available;
+ size_t produced;
+ int zret;
+
+ output_available = (uInt)(capacity - toc_length - 1);
+ strm.next_out = (unsigned char *)toc + toc_length;
+ strm.avail_out = output_available;
+ avail_in_before = strm.avail_in;
+
+ zret = inflate(&strm, Z_NO_FLUSH);
+ produced = output_available - strm.avail_out;
+
+ if (toc_length > SIZE_MAX - produced) {
+ cli_dbgmsg("cli_scanxar: Decompressed TOC length overflow.\n");
+ ret = CL_EFORMAT;
+ goto done;
+ }
+ toc_length += produced;
+
+ ret = cli_checklimits("cli_scanxar", ctx, toc_length, 0, 0);
+ if (ret != CL_SUCCESS) {
+ goto done;
+ }
+
+ if (zret == Z_STREAM_END) {
+ break;
+ }
+ if (zret != Z_OK) {
+ cli_dbgmsg("cli_scanxar: inflate failed with status %d.\n", zret);
+ ret = CL_EFORMAT;
+ goto done;
+ }
+ if (produced == 0 && strm.avail_in == avail_in_before) {
+ cli_dbgmsg("cli_scanxar: inflate made no progress before reaching the end of the TOC stream.\n");
+ ret = CL_EFORMAT;
+ goto done;
+ }
+
+ if (strm.avail_out == 0) {
+ char *new_toc;
+ size_t new_capacity;
+
+ if (capacity == CLI_MAX_ALLOCATION) {
+ cli_dbgmsg("cli_scanxar: Decompressed TOC exceeds the internal allocation limit.\n");
+ ret = CL_EFORMAT;
+ goto done;
+ }
+
+ new_capacity = capacity + XAR_TOC_INFLATE_CHUNK_SIZE;
+ if (new_capacity < capacity || new_capacity > CLI_MAX_ALLOCATION) {
+ new_capacity = CLI_MAX_ALLOCATION;
+ }
+
+ new_toc = cli_max_realloc(toc, new_capacity);
+ if (new_toc == NULL) {
+ cli_dbgmsg("cli_scanxar: Failed to grow the TOC buffer.\n");
+ ret = CL_EMEM;
+ goto done;
+ }
+ toc = new_toc;
+ capacity = new_capacity;
+ }
+ }
+
+ if (inflateEnd(&strm) != Z_OK) {
+ cli_dbgmsg("cli_scanxar: inflateEnd failed.\n");
+ ret = CL_EFORMAT;
+ goto done;
+ }
+ inflate_initialized = false;
+
+ toc[toc_length] = '\0';
+ *toc_out = toc;
+ *toc_length_out = toc_length;
+ toc = NULL;
+
+done:
+ if (inflate_initialized)
+ inflateEnd(&strm);
+ free(toc);
+ return ret;
+}
+
/*
cli_scanxar - scan an xar archive.
Parameters:
@@ -427,7 +547,8 @@
size_t length, offset, size, at;
int encoding;
z_stream strm;
- char *toc, *tmpname = NULL;
+ char *toc = NULL, *tmpname = NULL;
+ size_t toc_length = 0;
xmlTextReaderPtr reader = NULL;
int a_hash, e_hash;
unsigned char *a_cksum = NULL, *e_cksum = NULL;
@@ -435,8 +556,6 @@
char e_hash_result[SHA1_HASH_SIZE];
char a_hash_result[SHA1_HASH_SIZE];
- memset(&strm, 0x00, sizeof(z_stream));
-
/* retrieve xar header */
if (fmap_readn(ctx->fmap, &hdr, 0, sizeof(hdr)) != sizeof(hdr)) {
cli_dbgmsg("cli_scanxar: Invalid header, too short.\n");
@@ -463,46 +582,35 @@
/* cli_dbgmsg("hdr.toc_length_decompressed %lu\n", hdr.toc_length_decompressed); */
/* cli_dbgmsg("hdr.chksum_alg %i\n", hdr.chksum_alg); */
- /* Uncompress TOC */
- strm.next_in = (unsigned char *)fmap_need_off_once(ctx->fmap, hdr.size, hdr.toc_length_compressed);
- if (strm.next_in == NULL) {
- cli_dbgmsg("cli_scanxar: fmap_need_off_once fails on TOC.\n");
- return CL_EREAD;
- }
- strm.avail_in = hdr.toc_length_compressed;
- toc = cli_max_malloc(hdr.toc_length_decompressed + 1);
- if (toc == NULL) {
- cli_dbgmsg("cli_scanxar: cli_max_malloc fails on TOC decompress buffer.\n");
- return CL_EMEM;
+ /* Check time and file-count limits before inflating. Size limits are
+ * enforced against the actual decompressed TOC length below. */
+ rc = cli_checklimits("cli_scanxar", ctx, 0, 0, 0);
+ if (rc != CL_SUCCESS) {
+ return rc;
}
- toc[hdr.toc_length_decompressed] = '\0';
- strm.avail_out = hdr.toc_length_decompressed;
- strm.next_out = (unsigned char *)toc;
- rc = inflateInit(&strm);
- if (rc != Z_OK) {
- cli_dbgmsg("cli_scanxar:inflateInit error %i \n", rc);
- rc = CL_EFORMAT;
- goto exit_toc;
+
+ if (hdr.toc_length_compressed > SIZE_MAX) {
+ cli_dbgmsg("cli_scanxar: Compressed TOC length cannot be represented safely.\n");
+ return CL_EFORMAT;
}
- rc = inflate(&strm, Z_SYNC_FLUSH);
- if (rc != Z_OK && rc != Z_STREAM_END) {
- inflateEnd(&strm);
- cli_dbgmsg("cli_scanxar:inflate error %i \n", rc);
- rc = CL_EFORMAT;
- goto exit_toc;
+
+ /* Uncompress TOC */
+ {
+ const unsigned char *compressed_toc = fmap_need_off_once(ctx->fmap, hdr.size, (size_t)hdr.toc_length_compressed);
+
+ if (compressed_toc == NULL) {
+ cli_dbgmsg("cli_scanxar: fmap_need_off_once fails on TOC.\n");
+ return CL_EREAD;
+ }
+ rc = xar_inflate_toc(ctx, compressed_toc, (size_t)hdr.toc_length_compressed, &toc, &toc_length);
}
- rc = inflateEnd(&strm);
- if (rc != Z_OK) {
- cli_dbgmsg("cli_scanxar:inflateEnd error %i \n", rc);
- rc = CL_EFORMAT;
- goto exit_toc;
+ if (rc != CL_SUCCESS) {
+ return rc;
}
- if (hdr.toc_length_decompressed != strm.total_out) {
- cli_dbgmsg("TOC decompress length %" PRIu64 " does not match amount decompressed %lu\n",
- hdr.toc_length_decompressed, strm.total_out);
- toc[strm.total_out] = '\0';
- hdr.toc_length_decompressed = strm.total_out;
+ if (hdr.toc_length_decompressed != toc_length) {
+ cli_dbgmsg("TOC declared decompress length %" PRIu64 " does not match amount decompressed %zu\n",
+ hdr.toc_length_decompressed, toc_length);
}
/* cli_dbgmsg("cli_scanxar: TOC xml:\n%s\n", toc); */
@@ -512,7 +620,7 @@
/* scan the xml */
cli_dbgmsg("cli_scanxar: scanning xar TOC xml in memory.\n");
- rc = cli_magic_scan_buff(toc, hdr.toc_length_decompressed, ctx, NULL, LAYER_ATTRIBUTES_NONE);
+ rc = cli_magic_scan_buff(toc, toc_length, ctx, NULL, LAYER_ATTRIBUTES_NONE);
if (rc != CL_SUCCESS) {
goto exit_toc;
}
@@ -523,7 +631,7 @@
cli_dbgmsg("cli_scanxar: Can't create temporary file for TOC.\n");
goto exit_toc;
}
- if (cli_writen(fd, toc, hdr.toc_length_decompressed) == (size_t)-1) {
+ if (cli_writen(fd, toc, toc_length) == (size_t)-1) {
cli_dbgmsg("cli_scanxar: cli_writen error writing TOC.\n");
rc = CL_EWRITE;
xar_cleanup_temp_file(ctx, fd, tmpname);
@@ -535,7 +643,7 @@
goto exit_toc;
}
- reader = xmlReaderForMemory(toc, hdr.toc_length_decompressed, "noname.xml", NULL, CLAMAV_MIN_XMLREADER_FLAGS);
+ reader = xmlReaderForMemory(toc, toc_length, "noname.xml", NULL, CLAMAV_MIN_XMLREADER_FLAGS);
if (reader == NULL) {
cli_dbgmsg("cli_scanxar: xmlReaderForMemory error for TOC\n");
goto exit_toc;
diff -Nru clamav-1.4.5+dfsg/NEWS.md clamav-1.4.6+dfsg/NEWS.md
--- clamav-1.4.5+dfsg/NEWS.md 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/NEWS.md 2026-08-06 23:12:04.000000000 +0200
@@ -3,6 +3,86 @@
Note: This file refers to the official packages. Things described here may
differ slightly from third-party binary packages.
+## 1.4.6
+
+ClamAV 1.4.6 is a patch release with the following fixes:
+
+- [CVE-2026-20345](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-20345):
+ Fixed an indexing error while converting GPT partition names that could
+ read or write beyond a stack-allocated partition entry.
+
+ This issue affects ClamAV 0.98.2 through 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+ Thank you to Atuin - Automated Vulnerability Discovery Engine, Tianchu Chen
+ of Tencent Xuanwu Lab for identifying this issue.
+
+- [CVE-2026-20339](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-20339):
+ Fixed an integer overflow in the PESpin unpacker that could allocate an
+ undersized buffer and then write beyond it while rebuilding a PE file.
+
+ This issue affects ClamAV 0.90 through 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+ Thank you to Feng Xue and, independently, Yazdan Soltani for identifying
+ this issue.
+
+- [CVE-2026-20346](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-20346):
+ Fixed an integer underflow in the PDF parser that could cause a crash while
+ reading a malformed hex string.
+
+ This issue affects ClamAV 1.4.5 and earlier, and ClamAV 1.5.0 through
+ 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+ Thank you to Tristan (@TristanInSec) for identifying this issue.
+
+- [CVE-2026-20347](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-20347):
+ Fixed undefined behavior and integer overflow in the Mach-O parser that
+ could cause a crash while scanning a malformed Mach-O file.
+
+ This issue affects ClamAV 1.4.5 and earlier, and ClamAV 1.5.0 through
+ 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+ Thank you to Tristan (@TristanInSec) for identifying this issue.
+
+- [CVE-2026-20348](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-20348):
+ Fixed XAR parser size handling that could request an excessive allocation
+ or exceed scan limits while decompressing a malformed table of contents.
+
+ This issue affects ClamAV 0.98.1 through 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+ Thank you to leduckhuong for identifying this issue.
+
+- [CVE-2025-8088](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2025-8088):
+ Adopted the upstream UnRAR project fix in ClamAV's bundled UnRAR library.
+ The fix rejects path separators in NTFS alternate data stream names to
+ prevent extraction outside ClamAV's temporary scan directory on Windows.
+
+ This issue affects ClamAV 0.101.0 through 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+ Thank you to Yazdan Soltani for identifying that this issue affects ClamAV.
+
+- Fixed thread-safety issues in the `clamd` STATS command that could disclose
+ process memory or crash the daemon while scans and STATS requests run
+ concurrently. Also fixed partial socket-write handling used for large STATS
+ responses.
+
+ This issue affects ClamAV 0.95 through 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+- FreeBSD: Restored support for safe quarantine move and remove actions while
+ preserving protection against source-path replacement races.
+
+ This issue affects ClamAV 1.4.5 and 1.5.3.
+ The fix is included in 1.4.6 and 1.5.4.
+
+- Upgraded the Rust `crossbeam-epoch` dependency to resolve the
+ RUSTSEC-2026-0204 advisory.
+
## 1.4.5
ClamAV 1.4.5 is a patch release with the following fixes:
diff -Nru clamav-1.4.5+dfsg/unit_tests/check_clamd.c clamav-1.4.6+dfsg/unit_tests/check_clamd.c
--- clamav-1.4.5+dfsg/unit_tests/check_clamd.c 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/unit_tests/check_clamd.c 2026-08-06 23:12:04.000000000 +0200
@@ -62,6 +62,14 @@
// common
#include "fdpassing.h"
+#include "output.h"
+
+// clamd
+#include "clamd/thrmgr.h"
+
+/* Globals used by the thread manager's job-group shutdown checks. */
+pthread_mutex_t exit_mutex = PTHREAD_MUTEX_INITIALIZER;
+int progexit = 0;
static int conn_tcp(int port)
{
@@ -132,6 +140,45 @@
#endif
}
+static void close_socket(int socket_fd)
+{
+#ifndef _WIN32
+ close(socket_fd);
+#else
+ closesocket(socket_fd);
+#endif
+}
+
+static void create_tcp_socket_pair(int sockets[2])
+{
+ struct sockaddr_in address;
+ socklen_t address_len = sizeof(address);
+ int listener;
+
+ listener = socket(AF_INET, SOCK_STREAM, 0);
+ ck_assert_msg(listener != -1, "Unable to create listener socket: %s\n", strerror(errno));
+
+ memset(&address, 0, sizeof(address));
+ address.sin_family = AF_INET;
+ address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ address.sin_port = 0;
+
+ ck_assert_msg(bind(listener, (struct sockaddr *)&address, sizeof(address)) != -1,
+ "Unable to bind listener socket: %s\n", strerror(errno));
+ ck_assert_msg(getsockname(listener, (struct sockaddr *)&address, &address_len) != -1,
+ "Unable to get listener socket address: %s\n", strerror(errno));
+ ck_assert_msg(listen(listener, 1) != -1, "Unable to listen on socket: %s\n", strerror(errno));
+
+ sockets[0] = socket(AF_INET, SOCK_STREAM, 0);
+ ck_assert_msg(sockets[0] != -1, "Unable to create client socket: %s\n", strerror(errno));
+ ck_assert_msg(connect(sockets[0], (struct sockaddr *)&address, address_len) != -1,
+ "Unable to connect client socket: %s\n", strerror(errno));
+
+ sockets[1] = accept(listener, NULL, NULL);
+ ck_assert_msg(sockets[1] != -1, "Unable to accept socket connection: %s\n", strerror(errno));
+ close_socket(listener);
+}
+
#ifndef REPO_VERSION
#define REPO_VERSION VERSION
#endif
@@ -573,7 +620,7 @@
const char end[] = "zEND";
const char ping[] = "zPING";
- int dummyfd, i, killed = 0;
+ int dummyfd, i;
conn_setup();
dummyfd = open(SCANFILE, O_RDONLY);
ck_assert_msg(dummyfd != -1, "failed to open %s: %s\n", SCANFILE, strerror(errno));
@@ -581,14 +628,11 @@
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;
- }
+ (void)send(sockd, end, sizeof(end), 0);
conn_teardown();
conn_setup();
@@ -854,10 +898,530 @@
}
END_TEST
+struct stats_filename_test_state {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ int stage;
+};
+
+static void stats_filename_test_handler(void *data)
+{
+ struct stats_filename_test_state *state = data;
+ char filename[64] = "task-filename-before-stack-reuse";
+
+ thrmgr_setactivetask(filename, "TEST");
+
+ pthread_mutex_lock(&state->mutex);
+ state->stage = 1;
+ pthread_cond_broadcast(&state->cond);
+ while (state->stage < 2)
+ pthread_cond_wait(&state->cond, &state->mutex);
+
+ strcpy(filename, "task-filename-after-stack-reuse");
+ state->stage = 3;
+ pthread_cond_broadcast(&state->cond);
+ while (state->stage < 4)
+ pthread_cond_wait(&state->cond, &state->mutex);
+ pthread_mutex_unlock(&state->mutex);
+
+ thrmgr_setactivetask(NULL, NULL);
+
+ pthread_mutex_lock(&state->mutex);
+ state->stage = 5;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+}
+
+START_TEST(test_stats_owns_task_filename)
+{
+ struct stats_filename_test_state state;
+ threadpool_t *threadpool;
+ char *stats;
+ size_t stats_len;
+ int sockets[2];
+
+ memset(&state, 0, sizeof(state));
+ ck_assert_int_eq(pthread_mutex_init(&state.mutex, NULL), 0);
+ ck_assert_int_eq(pthread_cond_init(&state.cond, NULL), 0);
+
+ threadpool = thrmgr_new(1, 60, 1, stats_filename_test_handler);
+ ck_assert_ptr_nonnull(threadpool);
+ ck_assert_int_ne(thrmgr_dispatch(threadpool, &state), 0);
+
+ pthread_mutex_lock(&state.mutex);
+ while (state.stage < 1)
+ pthread_cond_wait(&state.cond, &state.mutex);
+ state.stage = 2;
+ pthread_cond_broadcast(&state.cond);
+ while (state.stage < 3)
+ pthread_cond_wait(&state.cond, &state.mutex);
+ pthread_mutex_unlock(&state.mutex);
+
+ create_tcp_socket_pair(sockets);
+ ck_assert_int_eq(thrmgr_printstats(sockets[1], '\n'), 0);
+ close_socket(sockets[1]);
+ stats = recvfull(sockets[0], &stats_len);
+ close_socket(sockets[0]);
+
+ ck_assert_msg(strstr(stats, "task-filename-before-stack-reuse") != NULL,
+ "STATS did not retain its owned task filename:\n%s", stats);
+ ck_assert_msg(strstr(stats, "task-filename-after-stack-reuse") == NULL,
+ "STATS read the task filename from reused caller storage:\n%s", stats);
+ free(stats);
+
+ pthread_mutex_lock(&state.mutex);
+ state.stage = 4;
+ pthread_cond_broadcast(&state.cond);
+ while (state.stage < 5)
+ pthread_cond_wait(&state.cond, &state.mutex);
+ pthread_mutex_unlock(&state.mutex);
+
+ thrmgr_destroy(threadpool);
+ pthread_cond_destroy(&state.cond);
+ pthread_mutex_destroy(&state.mutex);
+}
+END_TEST
+
+struct stats_slow_client_test_state {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ const char *filename;
+ int stage;
+};
+
+struct stats_print_thread_state {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ int socket_fd;
+ int result;
+ int started;
+ int done;
+};
+
+struct stats_pool_create_state {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ threadpool_t *threadpool;
+ int started;
+ int done;
+};
+
+static void stats_slow_client_test_handler(void *data)
+{
+ struct stats_slow_client_test_state *state = data;
+
+ thrmgr_setactivetask(state->filename, "TEST");
+
+ pthread_mutex_lock(&state->mutex);
+ state->stage = 1;
+ pthread_cond_broadcast(&state->cond);
+ while (state->stage < 2)
+ pthread_cond_wait(&state->cond, &state->mutex);
+ pthread_mutex_unlock(&state->mutex);
+
+ thrmgr_setactivetask(NULL, NULL);
+
+ pthread_mutex_lock(&state->mutex);
+ state->stage = 3;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+}
+
+static void stats_noop_handler(void *data)
+{
+ UNUSEDPARAM(data);
+}
+
+static void *stats_print_thread(void *data)
+{
+ struct stats_print_thread_state *state = data;
+
+ pthread_mutex_lock(&state->mutex);
+ state->started = 1;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+
+ state->result = thrmgr_printstats(state->socket_fd, '\n');
+ close_socket(state->socket_fd);
+
+ pthread_mutex_lock(&state->mutex);
+ state->done = 1;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+ return NULL;
+}
+
+static void *stats_pool_create_thread(void *data)
+{
+ struct stats_pool_create_state *state = data;
+
+ pthread_mutex_lock(&state->mutex);
+ state->started = 1;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+
+ state->threadpool = thrmgr_new(1, 60, 1, stats_noop_handler);
+
+ pthread_mutex_lock(&state->mutex);
+ state->done = 1;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+ return NULL;
+}
+
+START_TEST(test_stats_write_does_not_hold_pool_lock)
+{
+ struct stats_slow_client_test_state scan_state;
+ struct stats_print_thread_state print_state;
+ struct stats_pool_create_state pool_state;
+ threadpool_t *threadpool;
+ pthread_t print_thread;
+ pthread_t pool_thread;
+ struct timeval now;
+ struct timespec deadline;
+ char first_byte;
+ char *filename;
+ char *stats;
+ char *stats_tail;
+ size_t filename_size = (1024 * 1024) + 123;
+ size_t stats_len;
+ size_t stats_tail_len;
+ short int saved_send_timeout;
+ int send_buffer_size = 4096;
+ int socket_flags;
+ int sockets[2];
+ int print_completed_before_drain;
+ int pool_created_before_drain;
+ int stats_contains_filename;
+ int stats_has_end_marker;
+ int recv_result;
+
+ filename = malloc(filename_size + 1);
+ ck_assert_ptr_nonnull(filename);
+ memset(filename, 'A', filename_size);
+ filename[filename_size] = '\0';
+
+ memset(&scan_state, 0, sizeof(scan_state));
+ scan_state.filename = filename;
+ ck_assert_int_eq(pthread_mutex_init(&scan_state.mutex, NULL), 0);
+ ck_assert_int_eq(pthread_cond_init(&scan_state.cond, NULL), 0);
+
+ threadpool = thrmgr_new(1, 60, 1, stats_slow_client_test_handler);
+ ck_assert_ptr_nonnull(threadpool);
+ ck_assert_int_ne(thrmgr_dispatch(threadpool, &scan_state), 0);
+
+ pthread_mutex_lock(&scan_state.mutex);
+ while (scan_state.stage < 1)
+ pthread_cond_wait(&scan_state.cond, &scan_state.mutex);
+ pthread_mutex_unlock(&scan_state.mutex);
+
+ create_tcp_socket_pair(sockets);
+ ck_assert_int_eq(setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF,
+ (const char *)&send_buffer_size, sizeof(send_buffer_size)),
+ 0);
+
+ /* Match clamd's accepted client sockets and force mdprintf() to retry
+ * after a partial, nonblocking send. */
+ socket_flags = fcntl(sockets[1], F_GETFL, 0);
+ ck_assert_int_ne(socket_flags, -1);
+ ck_assert_int_ne(fcntl(sockets[1], F_SETFL, socket_flags | O_NONBLOCK), -1);
+
+ memset(&print_state, 0, sizeof(print_state));
+ print_state.socket_fd = sockets[1];
+ ck_assert_int_eq(pthread_mutex_init(&print_state.mutex, NULL), 0);
+ ck_assert_int_eq(pthread_cond_init(&print_state.cond, NULL), 0);
+
+ /* Keep the writer waiting longer than the pool-creation deadline so a
+ * send timeout cannot make the lock test pass. */
+ saved_send_timeout = mprintf_send_timeout;
+ mprintf_send_timeout = 900;
+ ck_assert_int_eq(pthread_create(&print_thread, NULL, stats_print_thread, &print_state), 0);
+
+ recv_result = recv(sockets[0], &first_byte, 1, 0);
+ ck_assert_int_eq(recv_result, 1);
+
+ pthread_mutex_lock(&print_state.mutex);
+ print_completed_before_drain = print_state.done;
+ pthread_mutex_unlock(&print_state.mutex);
+
+ memset(&pool_state, 0, sizeof(pool_state));
+ ck_assert_int_eq(pthread_mutex_init(&pool_state.mutex, NULL), 0);
+ ck_assert_int_eq(pthread_cond_init(&pool_state.cond, NULL), 0);
+ ck_assert_int_eq(pthread_create(&pool_thread, NULL, stats_pool_create_thread, &pool_state), 0);
+
+ pthread_mutex_lock(&pool_state.mutex);
+ while (!pool_state.started)
+ pthread_cond_wait(&pool_state.cond, &pool_state.mutex);
+ pthread_mutex_unlock(&pool_state.mutex);
+
+ gettimeofday(&now, NULL);
+ deadline.tv_sec = now.tv_sec;
+ deadline.tv_nsec = now.tv_usec * 1000 + 500000000;
+ if (deadline.tv_nsec >= 1000000000) {
+ deadline.tv_sec++;
+ deadline.tv_nsec -= 1000000000;
+ }
+
+ pthread_mutex_lock(&pool_state.mutex);
+ while (!pool_state.done) {
+ if (ETIMEDOUT == pthread_cond_timedwait(&pool_state.cond, &pool_state.mutex, &deadline))
+ break;
+ }
+ pool_created_before_drain = pool_state.done;
+ pthread_mutex_unlock(&pool_state.mutex);
+
+ stats_tail = recvfull(sockets[0], &stats_tail_len);
+ close_socket(sockets[0]);
+
+ ck_assert_int_eq(pthread_join(print_thread, NULL), 0);
+ ck_assert_int_eq(pthread_join(pool_thread, NULL), 0);
+ mprintf_send_timeout = saved_send_timeout;
+ ck_assert_int_eq(print_state.result, 0);
+ ck_assert_ptr_nonnull(pool_state.threadpool);
+
+ stats_len = stats_tail_len + 1;
+ stats = malloc(stats_len + 1);
+ ck_assert_ptr_nonnull(stats);
+ stats[0] = first_byte;
+ memcpy(stats + 1, stats_tail, stats_tail_len);
+ stats[stats_len] = '\0';
+ free(stats_tail);
+
+ stats_contains_filename = strstr(stats, filename) != NULL;
+ stats_has_end_marker = stats_len >= 4 && memcmp(stats + stats_len - 4, "END\n", 4) == 0;
+ free(stats);
+
+ pthread_cond_destroy(&print_state.cond);
+ pthread_mutex_destroy(&print_state.mutex);
+
+ thrmgr_destroy(pool_state.threadpool);
+ pthread_cond_destroy(&pool_state.cond);
+ pthread_mutex_destroy(&pool_state.mutex);
+
+ pthread_mutex_lock(&scan_state.mutex);
+ scan_state.stage = 2;
+ pthread_cond_broadcast(&scan_state.cond);
+ while (scan_state.stage < 3)
+ pthread_cond_wait(&scan_state.cond, &scan_state.mutex);
+ pthread_mutex_unlock(&scan_state.mutex);
+
+ thrmgr_destroy(threadpool);
+ pthread_cond_destroy(&scan_state.cond);
+ pthread_mutex_destroy(&scan_state.mutex);
+ free(filename);
+
+ ck_assert_msg(!print_completed_before_drain,
+ "STATS did not block on the intentionally non-reading client");
+ ck_assert_msg(pool_created_before_drain,
+ "STATS held the global pool lock while writing to a slow client");
+ ck_assert_msg(stats_contains_filename,
+ "STATS response did not include the complete active task filename");
+ ck_assert_msg(stats_has_end_marker,
+ "STATS response was truncated or contained data after its END marker");
+}
+END_TEST
+
+struct stats_worker_retirement_test_state {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ unsigned handled;
+};
+
+static void stats_worker_retirement_test_handler(void *data)
+{
+ struct stats_worker_retirement_test_state *state = data;
+
+ pthread_mutex_lock(&state->mutex);
+ state->handled++;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+}
+
+static void stats_test_sleep_millisecond(void)
+{
+#ifdef _WIN32
+ Sleep(1);
+#else
+ struct timespec delay = { .tv_sec = 0, .tv_nsec = 1000000L };
+ nanosleep(&delay, NULL);
+#endif
+}
+
+START_TEST(test_stats_does_not_strand_work_during_worker_retirement)
+{
+ struct stats_worker_retirement_test_state state;
+ struct stats_print_thread_state print_state;
+ struct task_desc *desc;
+ threadpool_t *threadpool;
+ pthread_t print_thread;
+ char *stats;
+ size_t stats_len;
+ unsigned i;
+ int sockets[2];
+ int worker_is_retiring = 0;
+ int second_job_handled = 0;
+
+ memset(&state, 0, sizeof(state));
+ ck_assert_int_eq(pthread_mutex_init(&state.mutex, NULL), 0);
+ ck_assert_int_eq(pthread_cond_init(&state.cond, NULL), 0);
+
+ threadpool = thrmgr_new(1, 1, 2, stats_worker_retirement_test_handler);
+ ck_assert_ptr_nonnull(threadpool);
+ ck_assert_int_ne(thrmgr_dispatch(threadpool, &state), 0);
+
+ pthread_mutex_lock(&state.mutex);
+ while (state.handled < 1)
+ pthread_cond_wait(&state.cond, &state.mutex);
+ pthread_mutex_unlock(&state.mutex);
+
+ /* Wait until the worker has returned to its timed idle wait, then hold
+ * its task descriptor so STATS retains pools_lock while snapshotting it. */
+ pthread_mutex_lock(&threadpool->pool_mutex);
+ while (threadpool->thr_idle < 1)
+ pthread_cond_wait(&threadpool->idle_cond, &threadpool->pool_mutex);
+ desc = threadpool->tasks;
+ ck_assert_ptr_nonnull(desc);
+ ck_assert_int_eq(pthread_mutex_lock(&desc->mutex), 0);
+ pthread_mutex_unlock(&threadpool->pool_mutex);
+
+ create_tcp_socket_pair(sockets);
+ memset(&print_state, 0, sizeof(print_state));
+ print_state.socket_fd = sockets[1];
+ ck_assert_int_eq(pthread_mutex_init(&print_state.mutex, NULL), 0);
+ ck_assert_int_eq(pthread_cond_init(&print_state.cond, NULL), 0);
+ ck_assert_int_eq(pthread_create(&print_thread, NULL, stats_print_thread, &print_state), 0);
+
+ pthread_mutex_lock(&print_state.mutex);
+ while (!print_state.started)
+ pthread_cond_wait(&print_state.cond, &print_state.mutex);
+ pthread_mutex_unlock(&print_state.mutex);
+
+ /* Once idle is zero but the sole worker is still alive, its timeout has
+ * fired and it is waiting for the global STATS lock before retiring. */
+ for (i = 0; i < 5000; i++) {
+ pthread_mutex_lock(&threadpool->pool_mutex);
+ worker_is_retiring = threadpool->thr_alive == 1 && threadpool->thr_idle == 0;
+ pthread_mutex_unlock(&threadpool->pool_mutex);
+ if (worker_is_retiring)
+ break;
+ stats_test_sleep_millisecond();
+ }
+ ck_assert_msg(worker_is_retiring,
+ "Worker did not reach the retirement checkpoint while STATS held the pool list lock");
+
+ /* The dispatcher sees the retiring worker as alive and therefore does
+ * not create a replacement. The worker must recheck the queue before it
+ * commits to exit. */
+ ck_assert_int_ne(thrmgr_dispatch(threadpool, &state), 0);
+ ck_assert_int_eq(pthread_mutex_unlock(&desc->mutex), 0);
+
+ for (i = 0; i < 5000; i++) {
+ pthread_mutex_lock(&state.mutex);
+ second_job_handled = state.handled == 2;
+ pthread_mutex_unlock(&state.mutex);
+ if (second_job_handled)
+ break;
+ stats_test_sleep_millisecond();
+ }
+
+ stats = recvfull(sockets[0], &stats_len);
+ close_socket(sockets[0]);
+ ck_assert_int_eq(pthread_join(print_thread, NULL), 0);
+
+ ck_assert_msg(second_job_handled,
+ "Work dispatched during worker retirement remained queued without a worker");
+ ck_assert_int_eq(print_state.result, 0);
+ ck_assert_ptr_nonnull(stats);
+ ck_assert_msg(stats_len >= 4 && memcmp(stats + stats_len - 4, "END\n", 4) == 0,
+ "STATS response was truncated during worker retirement:\n%s", stats);
+ free(stats);
+
+ thrmgr_destroy(threadpool);
+ pthread_cond_destroy(&print_state.cond);
+ pthread_mutex_destroy(&print_state.mutex);
+ pthread_cond_destroy(&state.cond);
+ pthread_mutex_destroy(&state.mutex);
+}
+END_TEST
+
+struct stats_queue_churn_test_state {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ unsigned handled;
+};
+
+static void stats_queue_churn_test_handler(void *data)
+{
+ struct stats_queue_churn_test_state *state = data;
+
+ /* Keep the queue active long enough for repeated snapshots to overlap
+ * with work-item removal. */
+#ifdef _WIN32
+ Sleep(1);
+#else
+ struct timespec delay = { .tv_sec = 0, .tv_nsec = 1000000L };
+ nanosleep(&delay, NULL);
+#endif
+
+ pthread_mutex_lock(&state->mutex);
+ state->handled++;
+ pthread_cond_broadcast(&state->cond);
+ pthread_mutex_unlock(&state->mutex);
+}
+
+START_TEST(test_stats_while_queue_is_changing)
+{
+ enum {
+ STATS_QUEUE_CHURN_JOBS = 256,
+ STATS_QUEUE_CHURN_SNAPSHOTS = 64
+ };
+ struct stats_queue_churn_test_state state;
+ threadpool_t *threadpool;
+ unsigned i;
+
+ memset(&state, 0, sizeof(state));
+ ck_assert_int_eq(pthread_mutex_init(&state.mutex, NULL), 0);
+ ck_assert_int_eq(pthread_cond_init(&state.cond, NULL), 0);
+
+ threadpool = thrmgr_new(1, 60, STATS_QUEUE_CHURN_JOBS * 2,
+ stats_queue_churn_test_handler);
+ ck_assert_ptr_nonnull(threadpool);
+
+ for (i = 0; i < STATS_QUEUE_CHURN_JOBS; i++)
+ ck_assert_int_ne(thrmgr_dispatch(threadpool, &state), 0);
+
+ for (i = 0; i < STATS_QUEUE_CHURN_SNAPSHOTS; i++) {
+ char *stats;
+ size_t stats_len;
+ int sockets[2];
+
+ create_tcp_socket_pair(sockets);
+ ck_assert_int_eq(thrmgr_printstats(sockets[1], '\n'), 0);
+ close_socket(sockets[1]);
+ stats = recvfull(sockets[0], &stats_len);
+ close_socket(sockets[0]);
+
+ ck_assert_ptr_nonnull(stats);
+ ck_assert_msg(stats_len >= 4 && memcmp(stats + stats_len - 4, "END\n", 4) == 0,
+ "STATS response was truncated while the work queue changed:\n%s", stats);
+ free(stats);
+ }
+
+ pthread_mutex_lock(&state.mutex);
+ while (state.handled < STATS_QUEUE_CHURN_JOBS)
+ pthread_cond_wait(&state.cond, &state.mutex);
+ pthread_mutex_unlock(&state.mutex);
+
+ thrmgr_destroy(threadpool);
+ pthread_cond_destroy(&state.cond);
+ pthread_mutex_destroy(&state.mutex);
+}
+END_TEST
+
static Suite *test_clamd_suite(void)
{
Suite *s = suite_create("clamd");
- TCase *tc_commands, *tc_stress;
+ TCase *tc_commands, *tc_stress, *tc_thrmgr;
tc_commands = tcase_create("clamd commands");
suite_add_tcase(s, tc_commands);
tcase_add_unchecked_fixture(tc_commands, commands_setup, commands_teardown);
@@ -887,6 +1451,13 @@
tcase_add_test(tc_stress, test_connections); // Disabled on Windows because test uses fork() instead of threads, and needs to be rewritten.
#endif
#endif
+ tc_thrmgr = tcase_create("thread manager");
+ suite_add_tcase(s, tc_thrmgr);
+ tcase_add_test(tc_thrmgr, test_stats_owns_task_filename);
+ tcase_add_test(tc_thrmgr, test_stats_write_does_not_hold_pool_lock);
+ tcase_add_test(tc_thrmgr, test_stats_does_not_strand_work_during_worker_retirement);
+ tcase_add_test(tc_thrmgr, test_stats_while_queue_is_changing);
+
return s;
}
diff -Nru clamav-1.4.5+dfsg/unit_tests/clamscan/quarantine_toctou_test.py clamav-1.4.6+dfsg/unit_tests/clamscan/quarantine_toctou_test.py
--- clamav-1.4.5+dfsg/unit_tests/clamscan/quarantine_toctou_test.py 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/unit_tests/clamscan/quarantine_toctou_test.py 2026-08-06 23:12:04.000000000 +0200
@@ -376,6 +376,62 @@
'stdout': completed.stdout,
}
+ def _exercise_source_hardlink_quarantine(self, action_mode: str):
+ assert action_mode in ('move', 'remove')
+
+ parent_dir = TC.path_tmp / ('src-hardlink-{}'.format(action_mode))
+ parent_dir.mkdir()
+
+ db_dir = TC.path_tmp / ('db-src-hardlink-{}'.format(action_mode))
+ db_dir.mkdir()
+
+ payload = b'CLAM-2959 quarantine source hard-link payload\n'
+ other_link_path = parent_dir / 'other-link.bin'
+ other_link_path.write_bytes(payload)
+
+ submitted_path = parent_dir / 'submitted-link.bin'
+ try:
+ os.link(other_link_path, submitted_path)
+ except OSError as err:
+ self.skipTest('Hard-link creation is not permitted in this test environment: {}'.format(err))
+
+ self._write_hdb_signature(db_dir / 'trigger.hdb', payload, 'CLAM-2959-SOURCE-HARDLINK')
+
+ quarantine_dir = None
+ command = []
+ if str(TC.valgrind):
+ command.append(str(TC.valgrind))
+ if TC.valgrind_args:
+ command.extend(TC.valgrind_args.split())
+ command.extend([str(TC.clamscan), '--debug', '-d', str(db_dir)])
+ if action_mode == 'remove':
+ command.append('--remove=yes')
+ else:
+ quarantine_dir = TC.path_tmp / 'quarantine-src-hardlink-move'
+ quarantine_dir.mkdir()
+ command.append('--move={}'.format(quarantine_dir))
+ command.append(str(submitted_path))
+
+ self.log.info('Starting clamscan command: %s', ' '.join(command))
+ completed = subprocess.run(
+ command,
+ cwd=str(TC.path_tmp),
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ universal_newlines=True,
+ check=False,
+ )
+ self.log.info('clamscan stdout:\n%s', completed.stdout)
+
+ return {
+ 'payload': payload,
+ 'submitted_path': submitted_path,
+ 'other_link_path': other_link_path,
+ 'quarantine_dir': quarantine_dir,
+ 'returncode': completed.returncode,
+ 'stdout': completed.stdout,
+ }
+
def _exercise_source_link_replacement_quarantine(self, action_mode: str, attempt: int):
assert action_mode in ('copy', 'move', 'remove')
@@ -426,6 +482,7 @@
command.append(str(link_path))
milestone_lines = [
+ 'action_source_populate_posix: Resolved action path for fd',
'cli_get_filepath_from_filedesc: File path for fd',
'cli_get_filepath_from_handle: File path for handle',
]
@@ -577,12 +634,12 @@
redirected_payload = redirect_dir / payload_path.name
quarantined_payload = quarantine_dir / payload_path.name
- copied_to_line = "{}: copied to '{}'".format(payload_path, quarantine_dir / payload_path.name)
+ copy_succeeded = ": copied to '" in stdout
self.assertFalse(
redirected_payload.exists(),
'Quarantine copy was redirected through the replaced directory entry.',
)
- if copied_to_line in stdout:
+ if copy_succeeded:
self.assertTrue(
quarantined_payload.exists(),
'Expected a reported successful quarantine copy to create the destination file.',
@@ -614,12 +671,12 @@
redirected_payload = redirect_dir / payload_path.name
quarantined_payload = quarantine_dir / payload_path.name
- moved_to_line = "{}: moved to '{}'".format(payload_path, quarantine_dir / payload_path.name)
+ move_succeeded = ": moved to '" in stdout
self.assertFalse(
redirected_payload.exists(),
'Quarantine move was redirected through the replaced directory entry.',
)
- if moved_to_line in stdout:
+ if move_succeeded:
self.assertFalse(
payload_path.exists(),
'Expected clamscan --move to remove the source file after quarantining it.',
@@ -691,6 +748,32 @@
self.assertFalse(payload_path.exists(), 'Expected quarantine removal to unlink the resolved target path.')
self.assertTrue(link_path.is_symlink(), 'Expected the original symlink entry to remain in place after the quarantine remove.')
+ @unittest.skipIf(operating_system == 'windows', 'This test covers POSIX descriptor path resolution.')
+ def test_quarantine_move_unlinks_submitted_hardlink(self):
+ self.step_name('Test quarantine move unlinks the submitted hard-link name')
+ result = self._exercise_source_hardlink_quarantine('move')
+
+ submitted_path = result['submitted_path']
+ other_link_path = result['other_link_path']
+ quarantined_path = result['quarantine_dir'] / submitted_path.name
+
+ self.assertEqual(1, result['returncode'], 'Expected a virus-found exit code from clamscan.')
+ self.assertFalse(submitted_path.exists(), 'Expected quarantine move to unlink the submitted hard-link name.')
+ self.assertTrue(other_link_path.exists(), 'Expected quarantine move to preserve the other hard-link name.')
+ self.assertEqual(result['payload'], other_link_path.read_bytes(), 'Expected the remaining hard link to retain the source bytes.')
+ self.assertTrue(quarantined_path.exists(), 'Expected quarantine move to create the destination file.')
+ self.assertEqual(result['payload'], quarantined_path.read_bytes(), 'Expected the quarantined file to contain the source bytes.')
+
+ @unittest.skipIf(operating_system == 'windows', 'This test covers POSIX descriptor path resolution.')
+ def test_quarantine_remove_unlinks_submitted_hardlink(self):
+ self.step_name('Test quarantine remove unlinks the submitted hard-link name')
+ result = self._exercise_source_hardlink_quarantine('remove')
+
+ self.assertEqual(1, result['returncode'], 'Expected a virus-found exit code from clamscan.')
+ self.assertFalse(result['submitted_path'].exists(), 'Expected quarantine remove to unlink the submitted hard-link name.')
+ self.assertTrue(result['other_link_path'].exists(), 'Expected quarantine remove to preserve the other hard-link name.')
+ self.assertEqual(result['payload'], result['other_link_path'].read_bytes(), 'Expected the remaining hard link to retain the source bytes.')
+
@unittest.skipIf(not hasattr(os, 'symlink'), 'This platform does not support symlink creation in the test environment.')
def test_quarantine_copy_does_not_act_on_replaced_source_link(self):
self.step_name('Test quarantine copy stays bound to the opened source object')
diff -Nru clamav-1.4.5+dfsg/unit_tests/clamscan/xar_test.py clamav-1.4.6+dfsg/unit_tests/clamscan/xar_test.py
--- clamav-1.4.5+dfsg/unit_tests/clamscan/xar_test.py 1970-01-01 02:00:00.000000000 +0200
+++ clamav-1.4.6+dfsg/unit_tests/clamscan/xar_test.py 2026-08-06 23:12:04.000000000 +0200
@@ -0,0 +1,153 @@
+# Copyright (C) 2026 Cisco Systems, Inc. and/or its affiliates. All rights reserved.
+
+"""
+Run clamscan XAR tests.
+"""
+
+import hashlib
+import re
+import struct
+import sys
+import zlib
+from pathlib import Path
+
+sys.path.append(str(Path(__file__).resolve().parents[1]))
+import testcase
+
+
+class TC(testcase.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TC, cls).setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ super(TC, cls).tearDownClass()
+
+ def setUp(self):
+ super(TC, self).setUp()
+
+ def tearDown(self):
+ super(TC, self).tearDown()
+ self.verify_valgrind_log()
+
+ def make_xar(
+ self,
+ name,
+ toc_length_decompressed,
+ toc=b'<xar></xar>',
+ truncate_stream=False,
+ pad_compressed_to=None,
+ ):
+ compressed_toc = zlib.compress(toc)
+ if truncate_stream:
+ compressed_toc = compressed_toc[:-4]
+ if pad_compressed_to is not None:
+ assert len(compressed_toc) <= pad_compressed_to
+ compressed_toc += bytes(pad_compressed_to - len(compressed_toc))
+
+ testfile = TC.path_tmp / name
+ testfile.write_bytes(
+ struct.pack(
+ '>IHHQQI',
+ 0x78617221,
+ 28,
+ 0,
+ len(compressed_toc),
+ toc_length_decompressed,
+ 0,
+ ) + compressed_toc
+ )
+ return testfile
+
+ def scan_xar(self, testfile, limits, path_db=None):
+ if path_db is None:
+ path_db = TC.path_source / 'unit_tests' / 'input' / 'clamav.hdb'
+
+ command = (
+ '{valgrind} {valgrind_args} {clamscan} {limits} '
+ '-d {path_db} {testfile}'
+ ).format(
+ valgrind=TC.valgrind,
+ valgrind_args=TC.valgrind_args,
+ clamscan=TC.clamscan,
+ limits=limits,
+ path_db=path_db,
+ testfile=testfile,
+ )
+ return self.execute_command(command)
+
+ def test_declared_large_actual_small_toc_is_scanned(self):
+ self.step_name('Test XAR limits use the actual TOC size')
+
+ for name, declared_length in (
+ ('allocation-limit-toc.xar', 1024 * 1024 * 1024),
+ ('overflow-toc.xar', (1 << 64) - 1),
+ ):
+ with self.subTest(declared_length=declared_length):
+ testfile = self.make_xar(
+ name,
+ declared_length,
+ pad_compressed_to=44,
+ )
+ assert testfile.stat().st_size == 72
+ output = self.scan_xar(
+ testfile,
+ '--alert-exceeds-max=yes --max-filesize=1M --max-scansize=1M',
+ )
+
+ assert output.ec == 0
+ self.verify_output(
+ output.out,
+ expected=[re.escape('{}: OK'.format(testfile))],
+ unexpected=[
+ 'Heuristics.Limits.Exceeded',
+ "Can't allocate memory ERROR",
+ ],
+ )
+
+ def test_declared_small_actual_large_toc_obeys_scan_limits(self):
+ self.step_name('Test XAR limits stop actual TOC output')
+
+ actual_toc = b'<xar>' + (b' ' * (2 * 1024 * 1024)) + b'</xar>'
+ testfile = self.make_xar(
+ 'actual-oversized-toc.xar',
+ 1,
+ toc=actual_toc,
+ )
+ output = self.scan_xar(
+ testfile,
+ '--alert-exceeds-max=yes --max-filesize=1M --max-scansize=1M',
+ )
+
+ assert output.ec == 1
+ self.verify_output(
+ output.out,
+ expected=['Heuristics.Limits.Exceeded.MaxScanSize FOUND'],
+ unexpected=["Can't allocate memory ERROR"],
+ )
+
+ def test_incomplete_toc_stream_is_not_scanned(self):
+ self.step_name('Test XAR requires a complete TOC stream')
+
+ toc = b'<xar></xar>'
+ path_db = TC.path_tmp / 'xar-toc.hdb'
+ path_db.write_text(
+ '{}:{}:XAR_TOC_TEST\n'.format(
+ hashlib.md5(toc).hexdigest(),
+ len(toc),
+ )
+ )
+ testfile = self.make_xar(
+ 'incomplete-toc.xar',
+ len(toc),
+ toc=toc,
+ truncate_stream=True,
+ )
+ output = self.scan_xar(testfile, '', path_db=path_db)
+
+ assert output.ec == 0
+ self.verify_output(
+ output.out,
+ unexpected=['XAR_TOC_TEST.UNOFFICIAL FOUND'],
+ )
diff -Nru clamav-1.4.5+dfsg/unit_tests/CMakeLists.txt clamav-1.4.6+dfsg/unit_tests/CMakeLists.txt
--- clamav-1.4.5+dfsg/unit_tests/CMakeLists.txt 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/unit_tests/CMakeLists.txt 2026-08-06 23:12:04.000000000 +0200
@@ -70,7 +70,10 @@
# check_clamd is used by the clamd tests
add_executable(check_clamd)
target_sources(check_clamd
- PRIVATE check_clamd.c checks.h)
+ PRIVATE
+ check_clamd.c
+ checks.h
+ ../clamd/thrmgr.c)
target_link_libraries(check_clamd
PRIVATE
ClamAV::libclamav
diff -Nru clamav-1.4.5+dfsg/unit_tests/valgrind.supp clamav-1.4.6+dfsg/unit_tests/valgrind.supp
--- clamav-1.4.5+dfsg/unit_tests/valgrind.supp 2026-07-01 08:45:49.000000000 +0200
+++ clamav-1.4.6+dfsg/unit_tests/valgrind.supp 2026-08-06 23:12:04.000000000 +0200
@@ -380,7 +380,7 @@
<image-fuzzy-hash-png-decoder>
Memcheck:Cond
...
- fun:read_header_info<std::io::cursor::Cursor<&[u8]>>
+ fun:read_header_info<*::io::cursor::Cursor<&[u8]>>
...
fun:fuzzy_hash_calculate_image
...
More information about the Pkg-clamav-devel
mailing list