[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, &section64, 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),
+                                                    &sections[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),
+                                                    &sections[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