diff --git a/Cargo.lock b/Cargo.lock
index d37272b..15eadd6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -59,12 +59,6 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
-[[package]]
-name = "arrayref"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
-
[[package]]
name = "arrayvec"
version = "0.7.8"
@@ -156,9 +150,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-lc-rs"
-version = "1.18.0"
+version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
dependencies = [
"aws-lc-sys",
"untrusted 0.7.1",
@@ -167,9 +161,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.44.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
dependencies = [
"cc",
"cmake",
@@ -237,16 +231,15 @@ dependencies = [
[[package]]
name = "blake3"
-version = "1.8.6"
+version = "1.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76ae7bad254120e9e4c63bafc385310756f90c484eac0e36b8317cf09cb92a77"
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
dependencies = [
- "arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
- "cpufeatures 0.3.0",
+ "cpufeatures 0.3.1",
"rayon-core",
]
@@ -361,9 +354,9 @@ dependencies = [
[[package]]
name = "cap-fs-ext"
-version = "4.0.2"
+version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d78e5a3368ae89b7cb68186411452b4b9fac8b41be9c19bf3f47c2d2c8e36e6b"
+checksum = "56ff379b70af8e08307a8f65e7040c7301cb4a572538ade16b4984f0da77847f"
dependencies = [
"cap-primitives",
"io-lifetimes 3.0.1",
@@ -372,9 +365,9 @@ dependencies = [
[[package]]
name = "cap-primitives"
-version = "4.0.2"
+version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0"
+checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18"
dependencies = [
"ambient-authority",
"fs-set-times",
@@ -423,9 +416,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.4.2"
+version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
+checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -468,12 +461,12 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
- "cpufeatures 0.3.0",
+ "cpufeatures 0.3.1",
"rand_core",
]
@@ -500,9 +493,9 @@ dependencies = [
[[package]]
name = "combine"
-version = "4.6.7"
+version = "4.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
dependencies = [
"bytes",
"memchr",
@@ -575,9 +568,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
@@ -599,9 +592,9 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
[[package]]
name = "crc32fast"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
dependencies = [
"cfg-if",
]
@@ -749,6 +742,37 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.20",
+]
+
[[package]]
name = "der-parser"
version = "10.0.0"
@@ -844,7 +868,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -944,9 +968,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
-version = "1.17.0"
+version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
dependencies = [
"serde",
]
@@ -960,7 +984,7 @@ dependencies = [
"cc",
"memchr",
"rustc_version",
- "toml 1.1.4+spec-1.1.0",
+ "toml 1.1.5+spec-1.1.0",
"vswhom",
"winreg",
]
@@ -1019,10 +1043,11 @@ dependencies = [
[[package]]
name = "eyre"
-version = "0.6.12"
+version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
+checksum = "c08309dbcc659c5549a24ddb9b27027640641b282ef5768267c7e675558986a3"
dependencies = [
+ "autocfg",
"indenter",
"once_cell",
]
@@ -1054,18 +1079,19 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
-version = "0.1.10"
+version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
+checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "flate2"
-version = "1.1.9"
+version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.9.1",
+ "zlib-rs",
]
[[package]]
@@ -1109,7 +1135,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -1146,9 +1172,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
dependencies = [
"futures-channel",
"futures-core",
@@ -1161,9 +1187,9 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
"futures-sink",
@@ -1171,15 +1197,15 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-executor"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
dependencies = [
"futures-core",
"futures-task",
@@ -1199,38 +1225,38 @@ dependencies = [
[[package]]
name = "futures-io"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-macro"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.4",
]
[[package]]
name = "futures-sink"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]]
name = "futures-task"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-channel",
"futures-core",
@@ -1638,9 +1664,9 @@ dependencies = [
[[package]]
name = "http-body-util"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
"bytes",
"futures-core",
@@ -1657,9 +1683,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hyper"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
dependencies = [
"atomic-waker",
"bytes",
@@ -1734,9 +1760,9 @@ dependencies = [
[[package]]
name = "icu_collections"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
@@ -1748,9 +1774,9 @@ dependencies = [
[[package]]
name = "icu_locale_core"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
@@ -1761,9 +1787,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -1775,16 +1801,17 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
+ "displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
@@ -1795,15 +1822,15 @@ dependencies = [
[[package]]
name = "icu_properties_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
-version = "2.2.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
@@ -1870,9 +1897,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.14.0"
+version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
@@ -1971,6 +1998,59 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "jiff"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
[[package]]
name = "jni"
version = "0.21.1"
@@ -2269,9 +2349,9 @@ dependencies = [
[[package]]
name = "libredox"
-version = "0.1.19"
+version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa"
+checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed"
dependencies = [
"libc",
]
@@ -2295,9 +2375,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "lock_api"
@@ -2310,9 +2390,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.33"
+version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "markup5ever"
@@ -2333,9 +2413,9 @@ checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
[[package]]
name = "mdns-sd"
-version = "0.20.3"
+version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86dbb9f00c8c367f75ed3a775d3eb31d0375a72f58275ef64a1bc53c255a2ce2"
+checksum = "b0a19dd805348943831582c4d9e6921c66de689127d6b27665a4e155c2117799"
dependencies = [
"fastrand",
"flume",
@@ -2393,10 +2473,20 @@ dependencies = [
]
[[package]]
-name = "mio"
-version = "1.2.2"
+name = "miniz_oxide"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
dependencies = [
"libc",
"log",
@@ -2492,9 +2582,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-integer"
-version = "0.1.46"
+version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
@@ -2763,9 +2853,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "open"
-version = "5.4.1"
+version = "5.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9cfef937e9c486488c7e3d949ae31c0f1d06bdacd75b99c086cb35356e30408"
+checksum = "7c603ab8300cf18bc3b14146b19fe3dfcc4843ae5a400cd0e7a30b95aa366634"
dependencies = [
"dunce",
"is-wsl",
@@ -2909,9 +2999,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "plist"
@@ -2920,7 +3010,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
dependencies = [
"base64 0.22.1",
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
"quick-xml",
"serde",
"time",
@@ -2936,7 +3026,7 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
- "miniz_oxide",
+ "miniz_oxide 0.8.9",
]
[[package]]
@@ -2949,14 +3039,29 @@ dependencies = [
"crc32fast",
"fdeflate",
"flate2",
- "miniz_oxide",
+ "miniz_oxide 0.8.9",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
]
[[package]]
name = "potential_utf"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
"zerovec",
]
@@ -3110,9 +3215,9 @@ dependencies = [
[[package]]
name = "rcgen"
-version = "0.14.8"
+version = "0.14.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055"
+checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8"
dependencies = [
"aws-lc-rs",
"rustls-pki-types",
@@ -3143,22 +3248,22 @@ dependencies = [
[[package]]
name = "ref-cast"
-version = "1.0.26"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
-version = "1.0.26"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -3335,9 +3440,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.103.13"
+version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"aws-lc-rs",
"ring",
@@ -3353,9 +3458,9 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "s2n-codec"
-version = "0.85.0"
+version = "0.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66aa14280ad931e7048e32dd2501966423ba5f9ec3fa8650fd31ad098fa5e303"
+checksum = "c5d0c1a0b6d0df7d940d18f18e16437fa7ddc9a42534ec945c39859bf32a8c2c"
dependencies = [
"byteorder",
"bytes",
@@ -3364,9 +3469,9 @@ dependencies = [
[[package]]
name = "s2n-quic"
-version = "1.85.0"
+version = "1.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d791203713d76de21c8e0396095667ce34bb560fc44af46d0d6e7fff1adb15be"
+checksum = "f1faf6becdc8ab36bc9efa074f2605aed06aa523aa294c49b194b187be6e14ef"
dependencies = [
"bytes",
"cfg-if",
@@ -3386,9 +3491,9 @@ dependencies = [
[[package]]
name = "s2n-quic-core"
-version = "0.85.0"
+version = "0.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "350907401b44da761ae7c2eb25252aceaaf6d47bd72826662d025bf6853106bd"
+checksum = "86796bee8a5989f7d8be770cc82543310c8330a90fa686f3dbc3875cf038bcc2"
dependencies = [
"atomic-waker",
"byteorder",
@@ -3408,9 +3513,9 @@ dependencies = [
[[package]]
name = "s2n-quic-crypto"
-version = "0.85.0"
+version = "0.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a687178dfcb7a19c4d58a7867169039b6d07cc8d9fab9395218bde19e209f93d"
+checksum = "5cefe8a51a8dcb9fd66c147afb6684283057f42ed150e5dd4864f69e8b85da4a"
dependencies = [
"aws-lc-rs",
"cfg-if",
@@ -3422,9 +3527,9 @@ dependencies = [
[[package]]
name = "s2n-quic-platform"
-version = "0.85.0"
+version = "0.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6dfb8b66f6f5a0b65e965505555d4830d1f559c858d6f84df86cf17eb1f57119"
+checksum = "4d8fab8fad9e739655cf0c6e1f017086424941deebc80d5eeed45992367e2f56"
dependencies = [
"cfg-if",
"futures",
@@ -3437,9 +3542,9 @@ dependencies = [
[[package]]
name = "s2n-quic-rustls"
-version = "0.85.0"
+version = "0.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82ac21eb7d17f40c236ca6bb22bd36aa1fdff3af4d2ab29fe4f46ad90aa4c756"
+checksum = "fa15ea6a3c4a62f7976c1d078ba6dd0527fd83351230a942b77ca96a642ac410"
dependencies = [
"bytes",
"rustls",
@@ -3451,9 +3556,9 @@ dependencies = [
[[package]]
name = "s2n-quic-transport"
-version = "0.85.0"
+version = "0.88.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39307614b59b4262689604176f58a914ab14dc94a1087611ac3f1190213d1d81"
+checksum = "65e6a9b09f0480df4b9f0430a89ab22b6c5630acb18248c92ee7befae5081119"
dependencies = [
"bytes",
"futures-channel",
@@ -3601,7 +3706,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -3636,7 +3741,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -3659,16 +3764,17 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.21.0"
+version = "3.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
+checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a"
dependencies = [
"base64 0.22.1",
"bs58",
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
+ "jiff",
"schemars 0.9.0",
"schemars 1.2.2",
"serde_core",
@@ -3679,9 +3785,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.21.0"
+version = "3.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
+checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46"
dependencies = [
"darling",
"proc-macro2",
@@ -3759,9 +3865,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "sigchld"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
+checksum = "24f2b37f04360cd465089b87a9c3869c08220a2f3458463f0adf8badf5e77f2c"
dependencies = [
"libc",
"os_pipe",
@@ -3770,9 +3876,9 @@ dependencies = [
[[package]]
name = "signal-hook"
-version = "0.3.18"
+version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
+checksum = "b2a0c28ca5908dbdbcd52e6fdaa00358ab88637f8ab33e1f188dd510eb44b53d"
dependencies = [
"libc",
"signal-hook-registry",
@@ -3808,9 +3914,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
-version = "1.15.2"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
[[package]]
name = "socket-pktinfo"
@@ -3920,7 +4026,7 @@ dependencies = [
"futures-util",
"hashbrown 0.16.1",
"hashlink",
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
"log",
"memchr",
"percent-encoding",
@@ -4059,9 +4165,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
-version = "1.0.7"
+version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
+checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e"
dependencies = [
"base64 0.21.7",
"serde",
@@ -4091,9 +4197,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "3.0.3"
+version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
dependencies = [
"proc-macro2",
"quote",
@@ -4321,9 +4427,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
-version = "2.7.2"
+version = "2.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940"
+checksum = "61854a36651aa48381e5e209f69a01273b77f3f9f91f0c430b1b98d33bd47229"
dependencies = [
"log",
"raw-window-handle",
@@ -4339,9 +4445,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
-version = "2.5.1"
+version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
+checksum = "de22eef34fd78c0da050e748710edd50bf127e651d02ea1b2bfada1523cc5c51"
dependencies = [
"anyhow",
"dunce",
@@ -4357,15 +4463,15 @@ dependencies = [
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.20",
- "toml 1.1.4+spec-1.1.0",
+ "toml 1.1.5+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-shell"
-version = "2.3.5"
+version = "2.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b"
+checksum = "8548af174c5516e4f71f142acea4d02e00316296ea9aafa58798851481003e3c"
dependencies = [
"encoding_rs",
"log",
@@ -4480,7 +4586,7 @@ dependencies = [
"serde_with",
"swift-rs",
"thiserror 2.0.20",
- "toml 1.1.4+spec-1.1.0",
+ "toml 1.1.5+spec-1.1.0",
"url",
"urlpattern",
"uuid",
@@ -4495,7 +4601,7 @@ checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6"
dependencies = [
"dunce",
"embed-resource",
- "toml 1.1.4+spec-1.1.0",
+ "toml 1.1.5+spec-1.1.0",
]
[[package]]
@@ -4544,7 +4650,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -4590,9 +4696,9 @@ dependencies = [
[[package]]
name = "tinystr"
-version = "0.8.3"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
@@ -4638,7 +4744,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.4",
]
[[package]]
@@ -4685,7 +4791,7 @@ version = "0.9.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
"serde_core",
"serde_spanned 1.1.1",
"toml_datetime 0.7.5+spec-1.1.0",
@@ -4696,11 +4802,11 @@ dependencies = [
[[package]]
name = "toml"
-version = "1.1.4+spec-1.1.0"
+version = "1.1.5+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
+checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
"serde_core",
"serde_spanned 1.1.1",
"toml_datetime 1.1.1+spec-1.1.0",
@@ -4742,7 +4848,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
"toml_datetime 0.6.3",
"winnow 0.5.40",
]
@@ -4753,7 +4859,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.3",
@@ -4766,7 +4872,7 @@ version = "0.25.13+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.1",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow 1.0.4",
@@ -5037,9 +5143,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
-version = "1.24.0"
+version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
+checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -5205,9 +5311,9 @@ dependencies = [
[[package]]
name = "web_atoms"
-version = "0.2.5"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297"
+checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"
dependencies = [
"phf",
"phf_codegen",
@@ -5832,9 +5938,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "writeable"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "wry"
@@ -6015,9 +6121,9 @@ dependencies = [
[[package]]
name = "zerotrie"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
@@ -6026,9 +6132,9 @@ dependencies = [
[[package]]
name = "zerovec"
-version = "0.11.6"
+version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
"yoke",
"zerofrom",
@@ -6037,15 +6143,21 @@ dependencies = [
[[package]]
name = "zerovec-derive"
-version = "0.11.3"
+version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.4",
]
+[[package]]
+name = "zlib-rs"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
+
[[package]]
name = "zmij"
version = "1.0.23"
diff --git a/Cargo.toml b/Cargo.toml
index 0f6b5f3..cab1920 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,11 +23,11 @@ futures = "0.3"
gethostname = "1"
if-addrs = "0.15"
log = "0.4"
-mdns-sd = "0.20"
+mdns-sd = "0.21"
mimalloc = { version = "0.1", features = ["secure"] }
rayon = "1"
rcgen = {
- version = "=0.14.8",
+ version = "=0.14.10",
default-features = false,
features = ["aws_lc_rs"]
}
@@ -38,7 +38,7 @@ rustls = {
features = ["aws-lc-rs", "logging", "std"]
}
s2n-quic = {
- version = "=1.85.0",
+ version = "=1.88.0",
default-features = false,
features = [
"provider-address-token-default",
@@ -46,7 +46,7 @@ s2n-quic = {
"provider-tls-rustls",
]
}
-s2n-quic-core = "=0.85.0"
+s2n-quic-core = "=0.88.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = {
diff --git a/SECURITY_AUDIT_2026-08-28_GEMINI-3.7-HIGH_TEAMWORK.md b/SECURITY_AUDIT_2026-08-28_GEMINI-3.7-HIGH_TEAMWORK.md
new file mode 100644
index 0000000..a7eed76
--- /dev/null
+++ b/SECURITY_AUDIT_2026-08-28_GEMINI-3.7-HIGH_TEAMWORK.md
@@ -0,0 +1,922 @@
+# Comprehensive Security Audit Report: lanspread P2P Game Library Sharing
+
+**Target System**: `lanspread` Peer-to-Peer Game Library Sharing Platform
+**Target Repository**: `/home/pfs/shm/ls`
+**Date of Audit**: 2026-08-28
+**Audit Scope**: All workspace crates (`crates/lanspread-*`) and Tauri GUI client (`crates/lanspread-tauri-deno-ts/`)
+**Assessment Type**: White-box Source Code Security Review, Threat Modeling, Architecture Analysis & Vulnerability Assessment
+
+---
+
+## Table of Contents
+
+1. [Executive Summary & Overall Security Posture](#1-executive-summary--overall-security-posture)
+2. [Threat Model & Attack Surface Breakdown](#2-threat-model--attack-surface-breakdown)
+ - [2.1 Adversary Model & Trust Assumptions](#21-adversary-model--trust-assumptions)
+ - [2.2 LAN mDNS Discovery & Peer Spoofing](#22-lan-mdns-discovery--peer-spoofing)
+ - [2.3 QUIC Transport, TLS 1.3 & Wire Protocol Framing](#23-quic-transport-tls-13--wire-protocol-framing)
+ - [2.4 Download Path Traversal, Chunk Verification & Archive Extraction](#24-download-path-traversal-chunk-verification--archive-extraction)
+ - [2.5 Tauri IPC Boundaries, Webview Isolation & Frontend Security](#25-tauri-ipc-boundaries-webview-isolation--frontend-security)
+ - [2.6 Database & Persistence Integrity](#26-database--persistence-integrity)
+3. [Audit Scope & Component Coverage](#3-audit-scope--component-coverage)
+4. [Comprehensive Findings Matrix](#4-comprehensive-findings-matrix)
+5. [Detailed Vulnerability & Hardening Write-ups](#5-detailed-vulnerability--hardening-write-ups)
+ - [5.1 P2P Networking, Discovery & Wire Protocol](#51-p2p-networking-discovery--wire-protocol)
+ - [Finding NET-01: Unauthenticated Inbound QUIC Streams & Forged Change Hints Trigger Reflected State-Pull Flooding (High)](#finding-net-01-unauthenticated-inbound-quic-streams--forged-change-hints-trigger-reflected-state-pull-flooding)
+ - [Finding NET-02: Missing IP Address & Port Validation in Discovered mDNS Services Enables Handshake Redirection to Multicast / Broadcast Targets (Medium)](#finding-net-02-missing-ip-address--port-validation-in-discovered-mdns-services-enables-handshake-redirection-to-multicast--broadcast-targets)
+ - [Finding NET-03: Inbound Request Framing Uses Excessive 8 MiB Limit Allowing Stream Memory Exhaustion DoS (Medium)](#finding-net-03-inbound-request-framing-uses-excessive-8-mib-limit-allowing-stream-memory-exhaustion-dos)
+ - [Finding NET-04: Global Capacity Cap in RecentCandidates Allows LAN Adversary to Deny Peer Discovery (Medium)](#finding-net-04-global-capacity-cap-in-recentcandidates-allows-lan-adversary-to-deny-peer-discovery)
+ - [Finding NET-05: Unbounded Collection Deserialization in Response::decode Prior to Semantic Bounds Validation (Low)](#finding-net-05-unbounded-collection-deserialization-in-responsedecode-prior-to-semantic-bounds-validation)
+ - [Finding NET-06: Unauthenticated Game Chunk and Stream-Install Egress on QUIC Server (Informational)](#finding-net-06-unauthenticated-game-chunk-and-stream-install-egress-on-quic-server)
+ - [5.2 File Transfer, Storage Safety, Chunk Verification & Archive Extraction](#52-file-transfer-storage-safety-chunk-verification--archive-extraction)
+ - [Finding EXP2-SEC-01: Unsafe Archive Extraction in SidecarUnpacker (Missing Post-Unpack Manifest Validation & Symlink Redirection Risk) (High)](#finding-exp2-sec-01-unsafe-archive-extraction-in-sidecarunpacker-missing-post-unpack-manifest-validation--symlink-redirection-risk)
+ - [Finding EXP2-SEC-02: Unbounded Archive Decompression (Zip/RAR Bomb) Leading to Host Disk Exhaustion Denial of Service (Medium)](#finding-exp2-sec-02-unbounded-archive-decompression-ziprar-bomb-leading-to-host-disk-exhaustion-denial-of-service)
+ - [Finding EXP2-SEC-03: Incomplete Path Sanitization in path_validation.rs (Omission of Windows Device Names & Trailing Dots/Spaces) (Medium)](#finding-exp2-sec-03-incomplete-path-sanitization-in-path_validationrs-omission-of-windows-device-names--trailing-dotsspaces)
+ - [Finding EXP2-SEC-04: Under-Constrained game_id Validation in lanspread-proto Wire Protocol Boundary (Medium)](#finding-exp2-sec-04-under-constrained-game_id-validation-in-lanspread-proto-wire-protocol-boundary)
+ - [Finding EXP2-SEC-05: Direct Pre-Verification Chunk Writes to Filesystem (Low / Defense-in-Depth)](#finding-exp2-sec-05-direct-pre-verification-chunk-writes-to-filesystem)
+ - [Finding EXP2-SEC-06: Predictable Fallback State Directory in World-Writable Shared /tmp Location (Low)](#finding-exp2-sec-06-predictable-fallback-state-directory-in-world-writable-shared-tmp-location)
+ - [Finding EXP2-SEC-07: Ambient File Creation in stream_install.rs Bypassing Capability-Based ConfinedGameRoot (Low / Hardening)](#finding-exp2-sec-07-ambient-file-creation-in-stream_installrs-bypassing-capability-based-confinedgameroot)
+ - [5.3 Tauri Desktop Shell, IPC Boundaries, Webview Isolation & Frontend Security](#53-tauri-desktop-shell-ipc-boundaries-webview-isolation--frontend-security)
+ - [Finding SEC-IPC-01: Elevated Administrator Execution of Untrusted P2P Batch Scripts with Fragile Parameter Parsing (High)](#finding-sec-ipc-01-elevated-administrator-execution-of-untrusted-p2p-batch-scripts-with-fragile-parameter-parsing)
+ - [Finding SEC-IPC-02: Disabled Content Security Policy (csp: null) in Tauri Webview Configuration (High)](#finding-sec-ipc-02-disabled-content-security-policy-csp-null-in-tauri-webview-configuration)
+ - [Finding SEC-IPC-03: Unvalidated game_id Path Resolution & Leftover Debug Macro in get_game_thumbnail (Medium)](#finding-sec-ipc-03-unvalidated-game_id-path-resolution--leftover-debug-macro-in-get_game_thumbnail)
+ - [Finding SEC-IPC-04: Unsandboxed External Archive Extraction (unrar) for Untrusted P2P Archives (Medium)](#finding-sec-ipc-04-unsandboxed-external-archive-extraction-unrar-for-untrusted-p2p-archives)
+ - [Finding SEC-IPC-05: Broad Webview Window Creation Capabilities (allow-create-webview-window) (Low)](#finding-sec-ipc-05-broad-webview-window-creation-capabilities-allow-create-webview-window)
+ - [Finding SEC-FE-01: Client-Side Regular Expression Denial of Service (ReDoS) in Log Viewers (Low)](#finding-sec-fe-01-client-side-regular-expression-denial-of-service-redos-in-log-viewers)
+ - [5.4 Database & Data Persistence Integrity](#54-database--data-persistence-integrity)
+ - [Finding SEC-DB-01: Missing Defensive SQLite PRAGMAs (trusted_schema = OFF) on Catalog Database Pool (Low / Defense-in-Depth)](#finding-sec-db-01-missing-defensive-sqlite-pragmas-trusted_schema--off-on-catalog-database-pool)
+6. [Prioritized Recommendations & Hardening Roadmap](#6-prioritized-recommendations--hardening-roadmap)
+7. [Codebase Health & Verification Results](#7-codebase-health--verification-results)
+
+---
+
+## 1. Executive Summary & Overall Security Posture
+
+`lanspread` is a peer-to-peer (P2P) desktop application designed for local area network (LAN) party environments, enabling nodes to discover neighboring peers via Multicast DNS (mDNS-SD), synchronize library metadata and real-time "Call to Play" events over QUIC/TLS 1.3, and download, unpack, and launch game packages directly across machines. The system is implemented as a Rust workspace with an asynchronous backend (`tokio`, `s2n-quic`, `sqlx`) and a desktop user interface built with Tauri (Vite, Deno, TypeScript, React).
+
+### Threat Environment
+The operational context of `lanspread` is a local network environment (LAN party, shared gaming lounge, university dormitory, or open Wi-Fi). In this environment:
+- Physical and link-layer network access is untrusted and unauthenticated.
+- Any participant on the subnet can inject raw UDP packets (including spoofed mDNS multicast on UDP 5353 and arbitrary QUIC UDP frames).
+- Any participant can establish direct QUIC connections to running peers, open bidirectional streams, and send arbitrary control or data payloads.
+- Peers may run modified, hostile versions of `lanspread` crafted to harvest metadata, exhaust node resources, disrupt network discovery, or deliver malicious archive content and scripts.
+
+### Major Architectural Strengths
+1. **Capability-Based Directory Confinement (`cap_primitives`)**: The core download engine enforces filesystem sandboxing using `ConfinedGameRoot` and handle-relative file operations with `FollowSymlinks::No`, preventing path traversal during standard chunk transfers.
+2. **Cryptographic Manifest Authority (BLAKE3)**: Game file manifests (`CatalogContentManifest`) enforce strict 4 MiB chunk-level and file-level BLAKE3 cryptographic digests. Corrupted or tampered chunks are detected during transfer.
+3. **Crash-Consistent Transactional File Management**: The download manager uses temporary write handles, journaled file ownership (`DownloadOwnershipTransaction`), and deferred sentinel writing (`version.ini`) to ensure incomplete downloads are never committed or published.
+4. **Parameterized Database Layer**: SQLite interactions in `lanspread-db` and `lanspread-compat` utilize `sqlx` prepared queries with parameter binding, eliminating classic SQL injection vulnerabilities.
+5. **Single Wire Protocol Policy**: By strictly disallowing legacy wire version fallbacks, backward-compatibility shims, and permissive serde escape hatches (`#[serde(other)]`), the wire protocol surface remains compact and auditable.
+
+### Key Security Posture Risks
+Despite these robust foundational designs, the security audit identified **20 distinct vulnerabilities and hardening deficiencies** (4 High, 7 Medium, 8 Low, 1 Informational). The primary risks include:
+- **Elevated Remote Code Execution via Untrusted P2P Scripts**: When launching games or starting dedicated servers on Windows, `lanspread` executes batch scripts (`game_setup.cmd`, `game_start.cmd`, `server_start.cmd`) downloaded from untrusted peers with elevated Administrator privileges (`runas` verb via `cmd.exe`).
+- **Reflected State-Pull Amplification & Unauthenticated Inbound Streams**: The QUIC server does not enforce Mutual TLS (mTLS) client authentication (`.with_no_client_auth()`). Unauthenticated attackers can inject forged `ChangeHint` messages impersonating other peers, triggering concurrent outbound QUIC connection floods directed at victim nodes.
+- **Disabled Webview Content Security Policy**: `tauri.conf.json` explicitly sets `"csp": null`, eliminating browser-level defenses against Cross-Site Scripting (XSS) and remote resource loading.
+- **Unchecked External Archive Extraction (`SidecarUnpacker`)**: Extraction of `.eti` (RAR) archives via the external `unrar` sidecar lacks post-extraction manifest verification, symlink auditing, and disk volume capacity preflight checks.
+
+```
++---------------------------------------------------------------------------------------------------+
+| LANSPREAD ARCHITECTURE |
+| |
+| +---------------------------------------------------------------------------------------------+ |
+| | FRONTEND (Vite / Deno / React) | |
+| | - UI Components, CtpChat, Log Viewers (ReDoS risk: SEC-FE-01) | |
+| +---------------------------------------------------------------------------------------------+ |
+| | IPC (invoke) |
+| v |
+| +---------------------------------------------------------------------------------------------+ |
+| | TAURI DESKTOP SHELL (src-tauri/) | |
+| | - Disabled CSP ("csp": null, SEC-IPC-02) | |
+| | - Elevated UAC batch execution (cmd.exe runas, SEC-IPC-01) | |
+| | - Unvalidated thumbnail path resolver (SEC-IPC-03) | |
+| | - Sidecar Unpacker spawning unrar (EXP2-SEC-01, EXP2-SEC-02, SEC-IPC-04) | |
+| +---------------------------------------------------------------------------------------------+ |
+| | | |
+| v v |
+| +---------------------------------------+ +-------------------------------------------+ |
+| | DATABASE & COMPAT | | CORE P2P BACKEND (lanspread-peer) | |
+| | - lanspread-db / lanspread-compat | | - QUIC Server (No mTLS: NET-01) | |
+| | - Parameterized sqlx (SEC-DB-01) | | - mDNS Discovery (Candidate DoS: NET-04) | |
+| | - Manifest parser & BLAKE3 digests | | - Download Engine (ConfinedGameRoot) | |
+| +---------------------------------------+ | - Stream Install (Ambient FS: EXP2-SEC-07)| |
+| +-------------------------------------------+ |
++---------------------------------------------------------------------------------------------------+
+```
+
+---
+
+## 2. Threat Model & Attack Surface Breakdown
+
+### 2.1 Adversary Model & Trust Assumptions
+- **LAN Adversary**: An attacker with network access on the local subnet (IP layer and broadcast/multicast domain). The adversary can capture, forge, and inject UDP packets, establish QUIC connections, and execute modified protocol implementations.
+- **Untrusted Peer Content**: Games, metadata, file chunks, and archive payloads hosted by remote peers must be treated as inherently untrusted. A malicious peer may attempt to deliver malicious code, craft directory traversal sequences, or exhaust victim resources.
+- **Compromised Webview**: A potential threat where malicious HTML/JS injected via peer metadata or chat messages attempts to escape the webview sandbox and invoke native host commands via Tauri IPC.
+
+### 2.2 LAN mDNS Discovery & Peer Spoofing
+- **Component**: `crates/lanspread-mdns`, `crates/lanspread-peer/src/services/discovery.rs`, `crates/lanspread-peer/src/services/advertise.rs`.
+- **Attack Surface**: Inbound multicast DNS responses received on UDP port 5353 (`_lanspread._udp.local.`).
+- **Threats**:
+ - **Peer Spoofing**: An attacker can advertise arbitrary `peer_id`s, IP addresses, and ports.
+ - **Discovery Denial-of-Service**: Saturating candidate rate limiters (`RecentCandidates`) to block legitimate peer discovery.
+ - **Target Redirection**: Announcing multicast, broadcast, loopback, or internal infrastructure IP addresses to force victim nodes to send QUIC handshake packets to unauthorized destinations.
+
+### 2.3 QUIC Transport, TLS 1.3 & Wire Protocol Framing
+- **Component**: `crates/lanspread-peer/src/{tls.rs, quic_runtime.rs, network.rs, services/stream.rs}`, `crates/lanspread-proto`.
+- **Attack Surface**: Inbound UDP datagrams on peer QUIC listening port (e.g. 42424), TLS 1.3 handshake negotiation, bidirectional stream framing.
+- **Threats**:
+ - **Unauthenticated Stream Injection**: Inbound connections have no client identity validation, allowing arbitrary anonymous nodes to send control commands.
+ - **Amplification & Reflection**: Forged state change hints causing the recipient to connect to third-party victim nodes.
+ - **Stream Memory Exhaustion**: Opening maximum permitted streams (64 globally) and pushing maximum frame lengths (8 MiB control frames) of buffered JSON data.
+
+### 2.4 Download Path Traversal, Chunk Verification & Archive Extraction
+- **Component**: `crates/lanspread-peer/src/{download/, stream_install.rs, install/transaction.rs, path_validation.rs}`, `crates/lanspread-utils`.
+- **Attack Surface**: Content manifests, game IDs, relative file paths, chunk streams, and downloaded `.eti` (RAR) archives.
+- **Threats**:
+ - **Path Traversal / Escape**: Crafting relative paths with `../`, UNC prefixes, drive letters, Windows device names (`CON`, `PRN`, `AUX`, `NUL`), or trailing dots/spaces to write outside the game directory.
+ - **Symlink Exploitation**: Archives containing symbolic links or NTFS directory junctions pointing to sensitive system locations.
+ - **Decompression Bombs**: Highly compressed archives that decompress to hundreds of gigabytes, exhausting host disk space.
+ - **Pre-Verification Chunk Write Corruption**: Writing unverified chunks directly to live files before completing hash checks.
+
+### 2.5 Tauri IPC Boundaries, Webview Isolation & Frontend Security
+- **Component**: `crates/lanspread-tauri-deno-ts/src-tauri/`, `crates/lanspread-tauri-deno-ts/src/`.
+- **Attack Surface**: 24 registered Tauri IPC commands, frontend DOM rendering, IPC parameter validation, webview capability configuration.
+- **Threats**:
+ - **Elevated Privilege Escalation**: Invoking batch scripts via Windows `cmd.exe` with Administrator (`runas`) UAC elevation.
+ - **Cross-Site Scripting (XSS)**: Execution of untrusted scripts in the webview context in the absence of a Content Security Policy (`"csp": null`).
+ - **IPC Parameter Abuse**: Passing path traversal strings into asset resolution commands (`get_game_thumbnail`).
+ - **Client-Side ReDoS**: Catastrophic backtracking in user-supplied regex filters in log windows.
+
+### 2.6 Database & Persistence Integrity
+- **Component**: `crates/lanspread-db`, `crates/lanspread-compat`.
+- **Attack Surface**: SQLite database file (`game.db`), content index (`catalog-content-index-v1.jsonl`), local state storage.
+- **Threats**:
+ - **SQL Injection**: Attempting to manipulate database queries via unsanitized strings (mitigated by sqlx parameter binding).
+ - **Corrupted Database Execution**: Exploiting SQLite features like trusted schema or custom functions in untrusted database files.
+
+---
+
+## 3. Audit Scope & Component Coverage
+
+Every crate under `crates/` and all frontend directories under `crates/lanspread-tauri-deno-ts/` were thoroughly evaluated during this security audit:
+
+| Component / Subsystem | Directory Path | Primary Responsibility | Audit Evaluation & Security Posture |
+|---|---|---|---|
+| `lanspread-peer` | `crates/lanspread-peer/` | Core P2P runtime: TLS, QUIC server/client, discovery, state sync, download manager, VFS, install transactions | **Core Focus**: Identified lack of mTLS (NET-01), mDNS IP validation gaps (NET-02), discovery candidate DoS (NET-04), archive extraction integrity (EXP2-SEC-01), and path sanitization omissions (EXP2-SEC-03). Download manager confinement (`ConfinedGameRoot`) is highly robust. |
+| `lanspread-proto` | `crates/lanspread-proto/` | Wire protocol data structures, framing constants, JSON codecs | **Evaluated**: Identified symmetric 8 MiB request frame limit (NET-03), unbounded collection deserialization (NET-05), and under-constrained wire `game_id` validation (EXP2-SEC-04). Strict serialization golden tests pass. |
+| `lanspread-mdns` | `crates/lanspread-mdns/` | Multicast DNS discovery wrapper (`mdns-sd`) | **Evaluated**: Evaluated service announcement and packet reception. Found missing semantic validation on extracted socket addresses (NET-02). |
+| `lanspread-db` | `crates/lanspread-db/` | SQLite schema, catalog content manifest models, BLAKE3 hash checks | **Evaluated**: Strong validation of path components, Windows device names, and NFC normalization. No SQL injection vulnerabilities found. |
+| `lanspread-compat` | `crates/lanspread-compat/` | Catalog bundle loading, SQLite queries, ETI migration glue | **Evaluated**: SQL queries use strict parameter bindings. Identified missing defensive SQLite PRAGMA (`trusted_schema = OFF`) (SEC-DB-01). |
+| `lanspread-utils` | `crates/lanspread-utils/` | Shared filesystem and hashing helpers | **Evaluated**: Audited for safe I/O operations and symlink protections. Conforms to security baseline. |
+| `lanspread-peer-cli` | `crates/lanspread-peer-cli/` | Scripted JSONL peer test harness and external unpacker | **Evaluated**: Audited JSONL command parser and external `unrar` invocation. Identified unsandboxed archive extraction (SEC-IPC-04). |
+| `lanspread-tauri-deno-ts` (Backend) | `crates/lanspread-tauri-deno-ts/src-tauri/` | Tauri shell, IPC command handlers, Windows script execution, sidecar unpacker | **Core Focus**: Identified elevated Administrator script execution (SEC-IPC-01), disabled CSP (SEC-IPC-02), unvalidated thumbnail resolution (SEC-IPC-03), and decompression bomb risks (EXP2-SEC-02). |
+| `lanspread-tauri-deno-ts` (Frontend) | `crates/lanspread-tauri-deno-ts/src/` | React/Deno/TS GUI client, CtpChat, log viewers, state stores | **Evaluated**: Evaluated React DOM escaping and state hydration. Identified client-side ReDoS in log window regex filters (SEC-FE-01). |
+
+---
+
+## 4. Comprehensive Findings Matrix
+
+The following table summarizes all 20 findings identified across the workspace, categorized by severity and vulnerability type:
+
+| Finding ID | Severity | Type | Affected Component | Primary Location | Finding Title |
+|---|---|---|---|---|---|
+| **NET-01** | **High** | Exploitable Vulnerability | `lanspread-peer` | `src/tls.rs:126-134`
`src/services/state_sync.rs:362-389` | Unauthenticated Inbound QUIC Streams & Forged Change Hints Trigger Reflected State-Pull Flooding |
+| **NET-02** | **Medium** | Exploitable Vulnerability | `lanspread-peer` / `lanspread-mdns` | `src/services/discovery.rs:393-412`
`lanspread-mdns/src/lib.rs:273-290` | Missing IP Address & Port Validation in Discovered mDNS Services Enables Handshake Redirection to Multicast / Broadcast Targets |
+| **NET-03** | **Medium** | Exploitable Vulnerability / DoS | `lanspread-proto` / `lanspread-peer` | `lanspread-proto/src/lib.rs:13`
`src/services/stream.rs:37-41` | Inbound Request Framing Uses Excessive 8 MiB Limit Allowing Stream Memory Exhaustion DoS |
+| **NET-04** | **Medium** | Exploitable Vulnerability / DoS | `lanspread-peer` | `src/services/discovery.rs:30-67, 300-309` | Global Capacity Cap in `RecentCandidates` Allows LAN Adversary to Deny Peer Discovery |
+| **NET-05** | **Low** | Defense-in-Depth / Resource Safety | `lanspread-proto` | `src/lib.rs:718-745, 811-834` | Unbounded Collection Deserialization in `Response::decode` Prior to Semantic Bounds Validation |
+| **NET-06** | **Informational** | Architectural Policy | `lanspread-peer` | `src/services/transfer.rs:258-341`
`src/services/stream.rs:273-303` | Unauthenticated Game Chunk and Stream-Install Egress on QUIC Server |
+| **EXP2-SEC-01** | **High** | Exploitable Vulnerability | `lanspread-peer` / `src-tauri` | `src/install/transaction.rs:461-468, 514-544`
`src-tauri/src/lib.rs:3195-3238` | Unsafe Archive Extraction in `SidecarUnpacker` (Missing Post-Unpack Manifest Validation & Symlink Redirection Risk) |
+| **EXP2-SEC-02** | **Medium** | Exploitable Vulnerability / DoS | `src-tauri` / `lanspread-peer` | `src-tauri/src/lib.rs:3166-3240`
`src/install/transaction.rs:514-544` | Unbounded Archive Decompression (Zip/RAR Bomb) Leading to Host Disk Exhaustion Denial of Service |
+| **EXP2-SEC-03** | **Medium** | Exploitable Vulnerability | `lanspread-peer` | `src/path_validation.rs:14-88` | Incomplete Path Sanitization in `path_validation.rs` (Omission of Windows Device Names & Trailing Dots/Spaces) |
+| **EXP2-SEC-04** | **Medium** | Defense-in-Depth / Validation Gap | `lanspread-proto` | `src/lib.rs:769-780, 912-923` | Under-Constrained `game_id` Validation in `lanspread-proto` Wire Protocol Boundary |
+| **EXP2-SEC-05** | **Low** | Defense-in-Depth / Crash Safety | `lanspread-peer` | `src/download/transport.rs:367-402` | Direct Pre-Verification Chunk Writes to Filesystem |
+| **EXP2-SEC-06** | **Low** | Hardening / Multi-user Safety | `lanspread-peer` | `src/state_paths.rs:18-32` | Predictable Fallback State Directory in World-Writable Shared `/tmp` Location |
+| **EXP2-SEC-07** | **Low** | Hardening / Architectural Parity | `lanspread-peer` | `src/stream_install.rs:1295-1316, 1895-1900` | Ambient File Creation in `stream_install.rs` Bypassing Capability-Based `ConfinedGameRoot` |
+| **SEC-IPC-01** | **High** | Exploitable Vulnerability / RCE | `lanspread-tauri-deno-ts` (src-tauri) | `src-tauri/src/lib.rs:1474-1513, 1867-1959, 2008-2064` | Elevated Administrator Execution of Untrusted P2P Batch Scripts with Fragile Parameter Parsing |
+| **SEC-IPC-02** | **High** | Defense-in-Depth / Isolation | `lanspread-tauri-deno-ts` (src-tauri) | `src-tauri/tauri.conf.json:20-22` | Disabled Content Security Policy (`csp: null`) in Tauri Webview Configuration |
+| **SEC-IPC-03** | **Medium** | Exploitable Vulnerability | `lanspread-tauri-deno-ts` (src-tauri) | `src-tauri/src/lib.rs:1534-1552` | Unvalidated `game_id` Path Resolution & Leftover Debug Macro in `get_game_thumbnail` |
+| **SEC-IPC-04** | **Medium** | Defense-in-Depth / Sandboxing | `lanspread-peer-cli` / `src-tauri` | `lanspread-peer-cli/src/lib.rs:344-370`
`src-tauri/src/lib.rs:3195-3221` | Unsandboxed External Archive Extraction (`unrar`) for Untrusted P2P Archives |
+| **SEC-IPC-05** | **Low** | Hardening / Least Privilege | `lanspread-tauri-deno-ts` (src-tauri) | `src-tauri/capabilities/default.json:11` | Broad Webview Window Creation Capabilities (`allow-create-webview-window`) |
+| **SEC-FE-01** | **Low** | Exploitable Vulnerability / UI DoS | `lanspread-tauri-deno-ts` (frontend) | `src/MainLogsWindow.tsx:160-164`
`src/UnpackLogsWindow.tsx:109-113` | Client-Side Regular Expression Denial of Service (ReDoS) in Log Viewers |
+| **SEC-DB-01** | **Low** | Defense-in-Depth / Database | `lanspread-compat` | `src/catalog_bundle.rs:67-75`
`src/eti.rs:29-45` | Missing Defensive SQLite PRAGMAs (`trusted_schema = OFF`) on Catalog Database Pool |
+
+---
+
+## 5. Detailed Vulnerability & Hardening Write-ups
+
+---
+
+### 5.1 P2P Networking, Discovery & Wire Protocol
+
+---
+
+#### Finding NET-01: Unauthenticated Inbound QUIC Streams & Forged Change Hints Trigger Reflected State-Pull Flooding
+
+- **Severity Rating**: **High** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:H)
+- **Category**: Exploitable Vulnerability / Authentication & Amplification
+- **Affected Component**: `crates/lanspread-peer` (TLS & State Sync Subsystems)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/tls.rs:126-134` (`server_provider`)
+ - `crates/lanspread-peer/src/services/server.rs:262-348` (`handle_peer_connection`)
+ - `crates/lanspread-peer/src/services/stream.rs:265-272` (`handle_peer_stream`)
+ - `crates/lanspread-peer/src/services/state_sync.rs:101-114, 362-389` (`hint_requires_pull`, `perform_refresh_for_peer`)
+
+##### Vulnerability Description & Root Cause Analysis
+In `tls.rs`, the QUIC server TLS configuration explicitly disables client certificate authentication:
+```rust
+pub(crate) fn server_provider(identity: &PeerIdentity) -> Result {
+ let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
+ let mut config = ServerConfig::builder_with_provider(provider)
+ .with_protocol_versions(&[&rustls::version::TLS13])?
+ .with_no_client_auth()
+ .with_single_cert(vec![identity.certificate()], identity.private_key())?;
+ harden_server_config(&mut config);
+ Ok(S2nRustlsServer::from(config))
+}
+```
+When inbound QUIC connections are established in `server.rs`, connecting clients are completely anonymous.
+
+When an inbound bidirectional stream delivers `Request::LibraryChanged(hint)` or `Request::CallToPlayChanged(hint)`, `handle_peer_stream` in `stream.rs` passes the `ChangeHint` structure directly to `ctx.state_sync.schedule_hint(...)` without verifying that the connection's sender owns `hint.claimed_peer_id`.
+
+In `state_sync.rs`:
+```rust
+async fn hint_requires_pull(ctx: &NetworkServiceCtx, trigger: HintTrigger) -> bool {
+ let snapshot = ctx
+ .peer_game_db
+ .read()
+ .await
+ .revision_snapshot(&trigger.hint.claimed_peer_id);
+ hint_requires_pull_from_snapshot(ctx.peer_id, trigger, snapshot.as_ref())
+}
+```
+If `claimed_peer_id` matches a known peer in `peer_game_db` and the hint specifies an incremented revision or new session ID, `hint_requires_pull` evaluates to `true`. This causes `state_sync` to immediately queue an outbound `perform_refresh_for_peer`, triggering a full outbound QUIC connection and `Hello` snapshot pull targeting the victim peer (`claimed_peer_id`).
+
+##### Threat & Impact Analysis
+1. **Reflected DoS / Network Amplification**: An attacker on the LAN can establish unauthenticated QUIC streams to every peer on the subnet and send forged `ChangeHint` messages claiming `Peer_Victim` has published revision `999999`. Every peer node on the network will simultaneously initiate an outbound QUIC connection to `Peer_Victim` and pull its full `HelloSnapshot`, exhausting `Peer_Victim`'s network bandwidth, QUIC connection limits, and CPU.
+2. **Unauthenticated Information Harvesting**: Any anonymous client can connect to any running peer and issue `Request::Hello` or `Request::Ping` to extract complete user display names, full shared game catalogs, and real-time Call-to-Play chat messages without presenting credentials.
+
+##### Concrete Attack Scenario / Reproduction Steps
+1. An attacker on the LAN listens to mDNS broadcasts and identifies `Peer_Victim` (`PeerId_V`, IP `192.168.1.50:42424`) and 30 other participating peers (`Peer_1` through `Peer_30`).
+2. The attacker opens a standard QUIC connection to `Peer_1` (`192.168.1.10:42424`).
+3. Over a bidirectional stream, the attacker sends:
+ ```json
+ {"LibraryChanged":{"claimed_peer_id":"","runtime_session_id":"00000000000000000000000000000000","revision":999999}}
+ ```
+4. `Peer_1` receives the frame, observes that `PeerId_V` has a new revision, and initiates `perform_refresh_for_peer(PeerId_V)`.
+5. The attacker repeats steps 2–3 across `Peer_2` through `Peer_30`.
+6. All 30 peers concurrently open QUIC connections to `Peer_Victim` and request full library snapshots, saturating `Peer_Victim`'s connection permits and CPU.
+
+##### Actionable Remediation Guidance
+1. **Enforce Mutual TLS (mTLS)**:
+ - Configure `ServerConfig` to require client certificates via a custom `ClientCertVerifier` that validates the client's Ed25519 self-signed certificate and extracts their public key.
+ - Configure `ClientConfig` to present the local `PeerIdentity` certificate during outbound handshakes.
+2. **Bind Inbound Commands to Verified Identity**:
+ - Extract the authenticated client `PeerId` from the TLS connection metadata in `handle_peer_connection`.
+ - In `stream.rs`, assert `hint.claimed_peer_id == authenticated_peer_id`. Reject and drop any hint where the claimed identity does not match the TLS identity.
+
+---
+
+#### Finding NET-02: Missing IP Address & Port Validation in Discovered mDNS Services Enables Handshake Redirection to Multicast / Broadcast Targets
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:M)
+- **Category**: Exploitable Vulnerability / Input Validation
+- **Affected Component**: `crates/lanspread-peer` (Discovery) / `crates/lanspread-mdns`
+- **File & Line References**:
+ - `crates/lanspread-peer/src/services/discovery.rs:393-412` (`validated_candidate_endpoint`)
+ - `crates/lanspread-mdns/src/lib.rs:273-290` (`MdnsBrowser::handle_service_resolved`)
+
+##### Vulnerability Description & Root Cause Analysis
+In `discovery.rs`:
+```rust
+fn validated_candidate_endpoint(info: &MdnsPeerInfo) -> Option {
+ if info.proto_ver != Some(PROTOCOL_VERSION) {
+ return None;
+ }
+ let Some(peer_id) = info.peer_id else {
+ return None;
+ };
+ Some(PeerEndpoint::new(peer_id, info.addr))
+}
+```
+Neither `lanspread-mdns` nor `discovery.rs` validates the socket address `info.addr`. The application does not check if `info.addr.ip()` is a multicast address (`224.0.0.0/4`, `ff00::/8`), a broadcast address (`255.255.255.255`), an unspecified address (`0.0.0.0`, `::`), or if `info.addr.port() == 0`.
+
+##### Threat & Impact Analysis
+An attacker broadcasting forged mDNS records can specify destination IP addresses pointing to multicast (`224.0.0.1`), broadcast (`255.255.255.255`), or internal infrastructure ports. When `DiscoveryWorker` processes these candidates, it spawns `run_protocol_negotiation` which calls `connector.connect(&endpoint)`. This causes `s2n-quic` to transmit UDP Initial handshake packets to multicast or broadcast addresses, polluting network segments and triggering unexpected socket errors.
+
+##### Concrete Attack Scenario / PoC
+1. Attacker transmits an mDNS response for service `_lanspread._udp.local.` with TXT record `proto_ver=8`, `peer_id=`, and host address `224.0.0.251:42424`.
+2. `MdnsBrowser` parses the record and sends an `MdnsService` event with `addr = 224.0.0.251:42424`.
+3. `validated_candidate_endpoint` accepts the endpoint.
+4. `run_peer_discovery` dispatches a QUIC connection attempt to `224.0.0.251:42424`.
+
+##### Actionable Remediation Guidance
+Implement an admissibility filter for discovered IP addresses and ports in `discovery.rs`:
+```rust
+fn is_admissible_peer_ip(ip: std::net::IpAddr) -> bool {
+ match ip {
+ std::net::IpAddr::V4(v4) => !v4.is_unspecified() && !v4.is_multicast() && !v4.is_broadcast(),
+ std::net::IpAddr::V6(v6) => !v6.is_unspecified() && !v6.is_multicast(),
+ }
+}
+
+fn validated_candidate_endpoint(info: &MdnsPeerInfo) -> Option {
+ if info.proto_ver != Some(PROTOCOL_VERSION) {
+ return None;
+ }
+ if info.addr.port() == 0 || !is_admissible_peer_ip(info.addr.ip()) {
+ log::debug!("Ignoring peer at {} with invalid IP/port", info.addr);
+ return None;
+ }
+ let Some(peer_id) = info.peer_id else {
+ return None;
+ };
+ Some(PeerEndpoint::new(peer_id, info.addr))
+}
+```
+
+---
+
+#### Finding NET-03: Inbound Request Framing Uses Excessive 8 MiB Limit Allowing Stream Memory Exhaustion DoS
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:M)
+- **Category**: Exploitable Vulnerability / Denial of Service
+- **Affected Component**: `crates/lanspread-proto`, `crates/lanspread-peer` (Stream & Quic Runtime)
+- **File & Line References**:
+ - `crates/lanspread-proto/src/lib.rs:13` (`MAX_CONTROL_FRAME_BYTES`)
+ - `crates/lanspread-peer/src/quic_runtime.rs:59, 83` (`quic_server_limits`)
+ - `crates/lanspread-peer/src/services/stream.rs:37-41` (`control_codec`)
+
+##### Vulnerability Description & Root Cause Analysis
+In `lanspread-proto/src/lib.rs`:
+```rust
+pub const MAX_CONTROL_FRAME_BYTES: usize = 8 * 1024 * 1024; // 8 MiB
+```
+In `stream.rs`:
+```rust
+fn control_codec() -> LengthDelimitedCodec {
+ LengthDelimitedCodec::builder()
+ .max_frame_length(MAX_CONTROL_FRAME_BYTES)
+ .new_codec()
+}
+```
+All valid inbound `Request` variants (`Ping`, `Hello`, `LibraryChanged`, `CallToPlayChanged`, `GetGameFileChunk`, `StreamInstall`) encode to less than 1,024 bytes. However, `handle_peer_stream` configures `FramedRead` using `MAX_CONTROL_FRAME_BYTES` (8 MiB) for incoming request streams. With `MAX_GLOBAL_CONTROL_STREAM_TASKS = 64`, up to 64 concurrent streams can each buffer up to 8 MiB before `serde_json` parsing fails.
+
+##### Threat & Impact Analysis
+An attacker opening 64 concurrent streams and pushing 8 MiB payloads of formatted JSON whitespace or long strings forces the victim process to allocate and buffer `64 * 8 MiB = 512 MiB` of raw payload in memory. On RAM-constrained gaming devices, this spike can induce out-of-memory (OOM) termination.
+
+##### Actionable Remediation Guidance
+Split `MAX_CONTROL_FRAME_BYTES` into asymmetric bounds for requests versus responses:
+```rust
+pub const MAX_REQUEST_FRAME_BYTES: usize = 64 * 1024; // 64 KiB
+pub const MAX_RESPONSE_FRAME_BYTES: usize = 8 * 1024 * 1024; // 8 MiB (for HelloSnapshot)
+```
+Configure `FramedRead` in `handle_peer_stream` to use `MAX_REQUEST_FRAME_BYTES`.
+
+---
+
+#### Finding NET-04: Global Capacity Cap in `RecentCandidates` Allows LAN Adversary to Deny Peer Discovery
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:M)
+- **Category**: Exploitable Vulnerability / Denial of Service
+- **Affected Component**: `crates/lanspread-peer` (Discovery)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/services/discovery.rs:30-67, 300-309` (`RecentCandidates`)
+
+##### Vulnerability Description & Root Cause Analysis
+In `discovery.rs`:
+```rust
+const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;
+
+impl RecentCandidates {
+ fn try_record(&mut self, candidate: PeerEndpoint, now: tokio::time::Instant) -> bool {
+ self.expire(now);
+ if self.entries.iter().any(|(endpoint, _)| {
+ endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr
+ }) || self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES
+ {
+ return false;
+ }
+ self.entries
+ .push_back((candidate, now + DISCOVERY_CANDIDATE_COOLDOWN));
+ true
+ }
+}
+```
+If `self.entries.len() >= 64`, `try_record` returns `false` for all newly discovered candidate endpoints. In `run_peer_discovery`, `!candidate_is_admissible(...)` causes the discovery loop to log a warning and discard the new peer.
+
+##### Threat & Impact Analysis
+An attacker broadcasting 64 fake mDNS service announcements with distinct `PeerId`s and ports will saturate `RecentCandidates` within milliseconds. Once saturated, all legitimate new peers joining the LAN will be rejected and ignored for the 5-second cooldown window. Repeating this flood every 5 seconds creates a permanent discovery blackout.
+
+##### Actionable Remediation Guidance
+Implement FIFO/LRU eviction in `RecentCandidates` when capacity is reached instead of dropping new candidates:
+```rust
+if self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES {
+ self.entries.pop_front();
+}
+self.entries.push_back((candidate, now + DISCOVERY_CANDIDATE_COOLDOWN));
+```
+
+---
+
+#### Finding NET-05: Unbounded Collection Deserialization in `Response::decode` Prior to Semantic Bounds Validation
+
+- **Severity Rating**: **Low** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)
+- **Category**: Defense-in-Depth / Resource Safety
+- **Affected Component**: `crates/lanspread-proto`
+- **File & Line References**:
+ - `crates/lanspread-proto/src/lib.rs:718-726, 739-745, 811-834`
+
+##### Vulnerability Description & Root Cause Analysis
+`Response::decode` executes `serde_json::from_slice::(bytes)` before calling semantic validation methods (`LibrarySnapshot::validate()`, `CallToPlayAuthorSnapshot::validate()`). A malicious peer can construct an 8 MiB response containing 100,000 minimal JSON elements in `Vec`, forcing full heap allocation before semantic validation rejects the payload.
+
+##### Actionable Remediation Guidance
+Apply streaming length checks or implement bounded deserialization helpers for large collections during the `serde` pass.
+
+---
+
+#### Finding NET-06: Unauthenticated Game Chunk and Stream-Install Egress on QUIC Server
+
+- **Severity Rating**: **Informational**
+- **Category**: Architectural Policy / Access Control
+- **Affected Component**: `crates/lanspread-peer` (Transfer & Stream Services)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/services/transfer.rs:258-341`
+ - `crates/lanspread-peer/src/services/stream.rs:273-303`
+
+##### Vulnerability Description & Root Cause Analysis
+`handle_peer_stream` allows any connected client to request arbitrary game file chunks (`Request::GetGameFileChunk`) or initiate streamed RAR installation (`Request::StreamInstall`). `admit_outbound_transfer` verifies that the game is in the catalog and ready, but performs no authentication or caller authorization.
+
+##### Actionable Remediation Guidance
+Combine with Finding NET-01 (mTLS) to record authenticated `PeerId`s for all file transfers, enabling audit logging and per-peer transfer throttling.
+
+---
+
+### 5.2 File Transfer, Storage Safety, Chunk Verification & Archive Extraction
+
+---
+
+#### Finding EXP2-SEC-01: Unsafe Archive Extraction in `SidecarUnpacker` (Missing Post-Unpack Manifest Validation & Symlink Redirection Risk)
+
+- **Severity Rating**: **High** (CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
+- **Category**: Exploitable Vulnerability / Path Traversal & Integrity
+- **Affected Component**: `crates/lanspread-peer` (`install/transaction.rs`), `crates/lanspread-tauri-deno-ts` (`src-tauri/src/lib.rs`)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/install/transaction.rs:461-468, 490-495, 514-544` (`install_inner`, `unpack_archives`)
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3195-3238` (`run_unrar_sidecar`)
+
+##### Vulnerability Description & Root Cause Analysis
+In `install/transaction.rs`, the game installation workflow executes:
+```rust
+let staging = installing_dir(game_root);
+prepare_owned_empty_dir(&staging)?;
+root_capability.sync_game_root()?;
+unpack_archives(game_root, &staging, unpacker, cancel_token).await?;
+rename_path(&staging, &local)
+ .wrap_err_with(|| format!("failed to promote install for {id}"))?;
+root_capability.sync_game_root()?;
+```
+In `src-tauri/src/lib.rs`, `run_unrar_sidecar` spawns `unrar x -p- archive.eti -y -o destination_dir`.
+When `unrar x` completes with exit status 0, `install_inner` immediately executes `rename_path(&staging, &local)`.
+
+**Root Cause**:
+1. There is **no post-extraction inspection or validation** of the files materialized in `staging`.
+2. The application does not check whether `unrar` created symlinks or Windows directory junctions pointing outside `staging` / `local`.
+3. The application does not verify extracted files against the catalog manifest's expected file list (`CatalogContentManifest`) or compute BLAKE3 hashes of extracted contents.
+4. Unlike `stream_install.rs` (which explicitly validates each entry against `CatalogExtractedEntry` and checks BLAKE3 hashes), the `.eti` archive path completely trusts the external `unrar` tool output.
+
+##### Threat & Impact Analysis
+If a malicious peer provides an `.eti` (RAR) archive containing symlinks (e.g. `link -> /home/victim/.ssh/` or `link -> C:\Windows\`), `unrar x` may extract these symlinks depending on the platform and unrar build. When `lanspread` later traverses `local/` during launch configuration (`apply_launch_settings_once`), it can read or modify files outside the game root.
+
+##### Concrete Attack Scenario / PoC
+1. Attacker creates an `.eti` archive containing:
+ - Entry 1: Symlink `config -> /home/victim/.config/`
+ - Entry 2: File `config/autostart.sh`
+2. Victim downloads and installs the game.
+3. `unrar x` extracts the symlink and payload into `.local.installing`.
+4. `install_inner` renames `.local.installing` to `local/` without inspecting contents.
+5. Subsequent operations traversing `local/` follow the symlink outside the game directory.
+
+##### Actionable Remediation Guidance
+Before calling `rename_path(&staging, &local)` in `install_inner`, execute a mandatory audit of `staging`:
+```rust
+for entry in walkdir::WalkDir::new(&staging) {
+ let entry = entry?;
+ let meta = std::fs::symlink_metadata(entry.path())?;
+ if meta.file_type().is_symlink() {
+ eyre::bail!("Unsafe symlink detected in extracted archive: {}", entry.path().display());
+ }
+}
+```
+Verify that all regular files match the expected sizes and BLAKE3 digests in the catalog manifest.
+
+---
+
+#### Finding EXP2-SEC-02: Unbounded Archive Decompression (Zip/RAR Bomb) Leading to Host Disk Exhaustion Denial of Service
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H)
+- **Category**: Exploitable Vulnerability / Denial of Service
+- **Affected Component**: `crates/lanspread-tauri-deno-ts` (`src-tauri`), `crates/lanspread-peer` (`install/transaction.rs`)
+- **File & Line References**:
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3166-3240` (`run_unrar_sidecar`)
+ - `crates/lanspread-peer/src/install/transaction.rs:514-544`
+
+##### Vulnerability Description & Root Cause Analysis
+In `run_unrar_sidecar`, `ScopedProcess::spawn` captures stdout/stderr up to `UNRAR_LOG_CAPTURE_LIMIT` (1 MB), but enforces **no limit on the volume of uncompressed data written to disk**. The application does not check available disk space prior to spawning `unrar`, nor does it monitor staging folder size during extraction.
+
+##### Threat & Impact Analysis
+An attacker delivering a highly compressed RAR bomb (e.g. a 20 MB archive that expands to 500 GB of zeros) will cause `unrar` to consume all available disk space (`ENOSPC`), causing system-wide application crashes, database corruption, and host instability.
+
+##### Actionable Remediation Guidance
+1. Query available disk space using `fs2::available_space` prior to extraction, ensuring `available_space >= expected_uncompressed_bytes + SAFETY_BUFFER`.
+2. Enforce a maximum uncompressed extraction ceiling (e.g., `MAX_CATALOG_TOTAL_BYTES`).
+
+---
+
+#### Finding EXP2-SEC-03: Incomplete Path Sanitization in `path_validation.rs` (Omission of Windows Device Names & Trailing Dots/Spaces)
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:M/A:L)
+- **Category**: Exploitable Vulnerability / Windows Path Traversal
+- **Affected Component**: `crates/lanspread-peer` (`path_validation.rs`)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/path_validation.rs:14-40` (`sanitize_relative_path`)
+ - `crates/lanspread-peer/src/path_validation.rs:46-88` (`validate_relative_path`)
+
+##### Vulnerability Description & Root Cause Analysis
+`path_validation.rs` checks for empty strings, null bytes, UNC prefixes (`//`), and drive separators (`:/`). However, unlike `download/manifest.rs:381-391` and `content_manifest/path.rs:194-204`:
+1. It does **not** check for Windows DOS reserved device names (`CON`, `PRN`, `AUX`, `NUL`, `COM1`..`COM9`, `LPT1`..`LPT9`).
+2. It does **not** reject components ending in dots or spaces (`.` or `' '`), which Win32 file APIs strip automatically (e.g. `file.txt.` becomes `file.txt`).
+3. It does not check for drive-relative paths such as `C:temp`.
+
+##### Threat & Impact Analysis
+On Windows hosts, creating or resolving files named `CON` or `AUX` causes file APIs to block indefinitely or interact with console devices, leading to Denial of Service or unhandled I/O failures.
+
+##### Actionable Remediation Guidance
+Harmonize `path_validation.rs` with `content_manifest/path.rs`:
+```rust
+fn validate_component(component: &str) -> eyre::Result<()> {
+ if component.is_empty() || matches!(component, "." | "..") {
+ eyre::bail!("Invalid component: {component:?}");
+ }
+ if component.ends_with([' ', '.']) {
+ eyre::bail!("Path component has trailing dot or space: {component}");
+ }
+ if is_windows_device_name(component) {
+ eyre::bail!("Path uses Windows device name: {component}");
+ }
+ Ok(())
+}
+```
+
+---
+
+#### Finding EXP2-SEC-04: Under-Constrained `game_id` Validation in `lanspread-proto` Wire Protocol Boundary
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)
+- **Category**: Defense-in-Depth / Input Validation
+- **Affected Component**: `crates/lanspread-proto`
+- **File & Line References**:
+ - `crates/lanspread-proto/src/lib.rs:769-780, 912-923` (`validate_game_id`)
+
+##### Vulnerability Description & Root Cause Analysis
+In `lanspread-proto`, wire requests (`Request::GetGameFileChunk`, `Request::StreamInstall`) validate `game_id` via:
+```rust
+fn validate_game_id(game_id: &str) -> Result<(), ControlValidationError> {
+ if game_id.trim().is_empty() {
+ return Err(ControlValidationError::EmptyField { field: "game ID" });
+ }
+ if game_id.len() > MAX_GAME_ID_BYTES {
+ return Err(ControlValidationError::FieldTooLong { field: "game ID", maximum: MAX_GAME_ID_BYTES });
+ }
+ Ok(())
+}
+```
+This allows path traversal characters (`../`, `/`, `\`), null bytes, and control characters through the wire protocol decoding step. In contrast, `download/manifest.rs` and `content_manifest/path.rs` enforce single component checks, NFC normalization, and reserved directory name rejection.
+
+##### Actionable Remediation Guidance
+Add character and component constraints to `validate_game_id` in `lanspread-proto`:
+```rust
+if game_id.contains(['/', '\\', '\0']) || game_id.contains("..") {
+ return Err(ControlValidationError::InvalidPathCharacters { field: "game ID" });
+}
+```
+
+---
+
+#### Finding EXP2-SEC-05: Direct Pre-Verification Chunk Writes to Filesystem
+
+- **Severity Rating**: **Low** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)
+- **Category**: Defense-in-Depth / Crash Consistency
+- **Affected Component**: `crates/lanspread-peer` (`download/transport.rs`)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/download/transport.rs:367-402` (`receive_chunk`)
+
+##### Vulnerability Description & Root Cause Analysis
+In `receive_chunk`, raw incoming bytes from peer streams are written directly to the target file via `write_chunk_bytes(&mut file, &bytes)` before `verifier.finish(...)` completes the BLAKE3 hash check. While uncommitted files are guarded by `version.ini` deferral and cleanup journals, a host crash or sudden power failure during an active download leaves corrupt chunk bytes on disk.
+
+##### Actionable Remediation Guidance
+Buffer the chunk in memory (chunks are up to 4 MiB) and compute the BLAKE3 hash before writing the verified buffer to disk.
+
+---
+
+#### Finding EXP2-SEC-06: Predictable Fallback State Directory in World-Writable Shared `/tmp` Location
+
+- **Severity Rating**: **Low** (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N)
+- **Category**: Hardening / Multi-User Safety
+- **Affected Component**: `crates/lanspread-peer` (`state_paths.rs`)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/state_paths.rs:18-32` (`resolve_state_dir`)
+
+##### Vulnerability Description & Root Cause Analysis
+If neither `LANSPREAD_STATE_DIR` nor `HOME`/`USERPROFILE` environment variables are set, `resolve_state_dir` falls back to `std::env::temp_dir().join("lanspread")` (`/tmp/lanspread`). On multi-user systems, `/tmp` is world-writable, allowing a local attacker to pre-create `/tmp/lanspread` with malicious permissions or symlinks.
+
+##### Actionable Remediation Guidance
+Incorporate the process UID into the fallback path on Unix (`/tmp/lanspread-`) and enforce `0o700` directory permissions upon creation.
+
+---
+
+#### Finding EXP2-SEC-07: Ambient File Creation in `stream_install.rs` Bypassing Capability-Based `ConfinedGameRoot`
+
+- **Severity Rating**: **Low** (CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)
+- **Category**: Hardening / Architectural Parity
+- **Affected Component**: `crates/lanspread-peer` (`stream_install.rs`)
+- **File & Line References**:
+ - `crates/lanspread-peer/src/stream_install.rs:1295-1316, 1895-1900`
+
+##### Vulnerability Description & Root Cause Analysis
+Unlike the standard download manager which uses capability-based `ConfinedGameRoot` with `FollowSymlinks::No`, `stream_install.rs` uses ambient `std::fs::create_dir_all` and `File::create(&path)`. While staging paths are validated as empty on initialization, using ambient filesystem APIs diverges from the project's capability-based confinement architecture.
+
+##### Actionable Remediation Guidance
+Refactor `StreamInstallReceiveState` to operate through `ConfinedGameRoot` or `MutationGameRoot` handles with `FollowSymlinks::No`.
+
+---
+
+### 5.3 Tauri Desktop Shell, IPC Boundaries, Webview Isolation & Frontend Security
+
+---
+
+#### Finding SEC-IPC-01: Elevated Administrator Execution of Untrusted P2P Batch Scripts with Fragile Parameter Parsing
+
+- **Severity Rating**: **High** (CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
+- **Category**: Exploitable Vulnerability / Remote Code Execution & Privilege Escalation
+- **Affected Component**: `crates/lanspread-tauri-deno-ts` (`src-tauri/src/lib.rs`)
+- **File & Line References**:
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1474-1487` (`sanitize_username`)
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1506-1513` (`script_params_with_mode`)
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1555-1582` (`run_as_admin_detached`)
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1818-1864` (`run_as_admin_and_wait`)
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1867-1959` (`run_game_windows`)
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2008-2064` (`start_server_windows`)
+
+##### Vulnerability Description & Root Cause Analysis
+When running a game or starting a dedicated server on Windows, `run_game_windows` and `start_server_windows` execute setup and launch scripts (`game_setup.cmd`, `game_start.cmd`, `server_start.cmd`) located in the downloaded game directory using `ShellExecuteExW` / `ShellExecuteW` with `lpVerb = "runas"`, triggering Administrator UAC elevation:
+```rust
+// src-tauri/src/lib.rs:1913-1918
+run_as_admin_and_wait(
+ "cmd.exe",
+ &setup_params,
+ &game_dir,
+ windows::Win32::UI::WindowsAndMessaging::SW_HIDE,
+)
+```
+These scripts originate from untrusted remote P2P peers.
+
+Furthermore, parameters are assembled via:
+```rust
+format!(
+ r#"/d /s {cmd_mode} ""{}" "local" "{}" "{}" "{}"""#,
+ script_path.display(),
+ id,
+ settings.language,
+ settings.username,
+)
+```
+`sanitize_username` only strips control characters, `"`, and `%` (`!c.is_control() && *c != '"' && *c != '%'`). It does **not** filter shell meta-characters such as `&`, `|`, `^`, `<`, `>`, `(`, `)`.
+
+##### Threat & Impact Analysis
+- **Elevated Remote Code Execution**: An attacker sharing a game package on the LAN containing a malicious `game_setup.cmd` achieves full Administrator execution on the victim machine as soon as the user clicks "Play".
+- **Parameter Injection**: Unsanitized username or language values containing `&` or `|` can trigger arbitrary command chaining inside `cmd.exe`.
+
+##### Concrete Attack Scenario / PoC
+1. Attacker hosts a game on the LAN with a crafted `game_setup.cmd` that executes administrative commands (e.g. modifying system registry or installing persistent services).
+2. Victim downloads the game via P2P and clicks "Play".
+3. `lanspread` prompts the user with a standard Windows UAC dialog (invoking `cmd.exe` as Administrator).
+4. Upon user confirmation, the attacker's script executes with full Local Administrator privileges.
+
+##### Actionable Remediation Guidance
+1. **Enforce Cryptographic Script Verification**: Before executing any `.cmd` or executable file, verify its exact BLAKE3 digest against the authoritative `CatalogContentManifest`. Reject execution if the hash does not match.
+2. **Strict Username Whitelist**: Constrain username and language parameters to `[a-zA-Z0-9_-]`.
+3. **Avoid Shell Invocation**: Execute target binaries directly rather than wrapping them in `cmd.exe /c` where feasible.
+
+---
+
+#### Finding SEC-IPC-02: Disabled Content Security Policy (`csp: null`) in Tauri Webview Configuration
+
+- **Severity Rating**: **High** (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N)
+- **Category**: Defense-in-Depth / Webview Isolation
+- **Affected Component**: `crates/lanspread-tauri-deno-ts` (`src-tauri/tauri.conf.json`)
+- **File & Line References**:
+ - `crates/lanspread-tauri-deno-ts/src-tauri/tauri.conf.json:20-22`
+
+##### Vulnerability Description & Root Cause Analysis
+In `tauri.conf.json`:
+```json
+"app": {
+ "security": {
+ "csp": null
+ }
+}
+```
+Setting `"csp": null` explicitly disables Tauri's Content Security Policy injection.
+
+##### Threat & Impact Analysis
+Without a CSP, the webview renderer has no browser-level restrictions on network connections, script sources, or object embedding. If an XSS vulnerability occurs anywhere in the frontend (e.g. via peer chat messages, manipulated game metadata, or a compromised frontend dependency), an attacker can execute arbitrary scripts, connect to external servers, or invoke accessible Tauri IPC commands.
+
+##### Actionable Remediation Guidance
+Define a strict Content Security Policy in `tauri.conf.json`:
+```json
+"security": {
+ "csp": "default-src 'self'; img-src 'self' data: asset:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src ipc:; frame-src 'none'; object-src 'none'; base-uri 'none';"
+}
+```
+
+---
+
+#### Finding SEC-IPC-03: Unvalidated `game_id` Path Resolution & Leftover Debug Macro in `get_game_thumbnail`
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N)
+- **Category**: Exploitable Vulnerability / Path Resolution
+- **Affected Component**: `crates/lanspread-tauri-deno-ts` (`src-tauri/src/lib.rs`)
+- **File & Line References**:
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1534-1552` (`get_game_thumbnail`)
+
+##### Vulnerability Description & Root Cause Analysis
+`get_game_thumbnail` is implemented as:
+```rust
+#[tauri::command]
+async fn get_game_thumbnail(
+ game_id: String,
+ app_handle: tauri::AppHandle,
+ state: tauri::State<'_, LanSpreadState>,
+) -> tauri::Result {
+ use base64::Engine;
+
+ let _app_invoke = enter_app_invoke(state.inner())?;
+ let resource_path = app_handle.path().resolve(
+ format!("assets/{game_id}.jpg"),
+ tauri::path::BaseDirectory::Resource,
+ )?;
+
+ dbg!(&resource_path);
+
+ let image_data = scoped_blocking(|| std::fs::read(&resource_path))?;
+ let base64_data = base64::engine::general_purpose::STANDARD.encode(&image_data);
+ Ok(format!("data:image/jpeg;base64,{base64_data}"))
+}
+```
+Unlike `run_game_windows`, `get_game_thumbnail` does not call `is_single_component_game_id(&game_id)`. Passing `../` sequences allows resolving and reading arbitrary `.jpg` files from the resource bundle. Additionally, line 1547 contains `dbg!(&resource_path)`, which emits filesystem paths to stderr in release builds.
+
+##### Actionable Remediation Guidance
+Validate `game_id` with `is_single_component_game_id(&game_id)` and remove the `dbg!` macro.
+
+---
+
+#### Finding SEC-IPC-04: Unsandboxed External Archive Extraction (`unrar`) for Untrusted P2P Archives
+
+- **Severity Rating**: **Medium** (CVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:U/C:M/I:M/A:N)
+- **Category**: Defense-in-Depth / Subprocess Sandboxing
+- **Affected Component**: `crates/lanspread-peer-cli`, `crates/lanspread-tauri-deno-ts` (`src-tauri`)
+- **File & Line References**:
+ - `crates/lanspread-peer-cli/src/lib.rs:344-370` (`ExternalUnrarUnpacker`)
+ - `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3195-3221` (`run_unrar_sidecar`)
+
+##### Vulnerability Description & Root Cause Analysis
+`ExternalUnrarUnpacker` and `run_unrar_sidecar` invoke `binaries/unrar` directly without OS-level sandboxing (e.g. Landlock, pledge, AppContainer) or symlink stripping flags (`-sl-`). If an untrusted RAR contains directory traversal paths or symlinks, unrar could write outside the destination directory.
+
+##### Actionable Remediation Guidance
+Pass symlink-disabling flags (`-sl-`) to `unrar` and verify that all extracted files reside strictly within the destination staging directory.
+
+---
+
+#### Finding SEC-IPC-05: Broad Webview Window Creation Capabilities (`allow-create-webview-window`)
+
+- **Severity Rating**: **Low** (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N)
+- **Category**: Hardening / Least Privilege
+- **Affected Component**: `crates/lanspread-tauri-deno-ts` (`src-tauri/capabilities/default.json`)
+- **File & Line References**:
+ - `crates/lanspread-tauri-deno-ts/src-tauri/capabilities/default.json:11`
+
+##### Vulnerability Description & Root Cause Analysis
+`capabilities/default.json` grants `"core:webview:allow-create-webview-window"` to the default webview. If an XSS vulnerability occurs, script injection could dynamically spawn unmonitored browser windows.
+
+##### Actionable Remediation Guidance
+Statically declare companion windows (`main-logs`, `unpack-logs`) in `tauri.conf.json` and remove dynamic window creation permissions from `default.json`.
+
+---
+
+#### Finding SEC-FE-01: Client-Side Regular Expression Denial of Service (ReDoS) in Log Viewers
+
+- **Severity Rating**: **Low** (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L)
+- **Category**: Exploitable Vulnerability / UI DoS
+- **Affected Component**: `crates/lanspread-tauri-deno-ts` (Frontend)
+- **File & Line References**:
+ - `crates/lanspread-tauri-deno-ts/src/MainLogsWindow.tsx:160-164`
+ - `crates/lanspread-tauri-deno-ts/src/UnpackLogsWindow.tsx:109-113`
+
+##### Vulnerability Description & Root Cause Analysis
+User-supplied filter strings are compiled via `new RegExp(regexInput, 'i')` and executed synchronously over thousands of log lines on every keypress. A pattern with nested quantifiers (e.g. `(a+)+$`) triggers catastrophic backtracking, freezing the UI thread.
+
+##### Actionable Remediation Guidance
+Enforce a length limit on `regexInput`, debounce execution, or execute regex evaluation inside a Web Worker.
+
+---
+
+### 5.4 Database & Data Persistence Integrity
+
+---
+
+#### Finding SEC-DB-01: Missing Defensive SQLite PRAGMAs (`trusted_schema = OFF`) on Catalog Database Pool
+
+- **Severity Rating**: **Low** (CVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N)
+- **Category**: Defense-in-Depth / Database Hardening
+- **Affected Component**: `crates/lanspread-compat`
+- **File & Line References**:
+ - `crates/lanspread-compat/src/catalog_bundle.rs:67-75`
+ - `crates/lanspread-compat/src/eti.rs:29-45`
+
+##### Vulnerability Description & Root Cause Analysis
+When opening SQLite database pools, `SqliteConnectOptions` configures `.read_only(true)`, but does not explicitly set `PRAGMA trusted_schema = OFF;` or `PRAGMA cell_size_check = ON;`. Setting `trusted_schema = OFF` ensures that SQLite triggers or virtual table functions in loaded databases cannot execute untrusted code.
+
+##### Actionable Remediation Guidance
+Configure connection pools with defensive SQLite PRAGMAs:
+```rust
+SqliteConnectOptions::new()
+ .filename(db_path)
+ .read_only(true)
+ .pragma("trusted_schema", "OFF")
+ .pragma("cell_size_check", "ON")
+```
+
+---
+
+## 6. Prioritized Recommendations & Hardening Roadmap
+
+To systematically resolve the identified findings, the remediation efforts are prioritized into three actionable tiers:
+
+### Tier 1: Immediate Remediation (High Priority / P0)
+*Target Timeline: Immediate release / next sprint*
+1. **Mitigate Elevated Batch Script Execution (SEC-IPC-01)**:
+ - Enforce cryptographic BLAKE3 manifest verification for all `.cmd` and `.exe` scripts prior to execution.
+ - Enforce strict username/language character sanitization (`[a-zA-Z0-9_-]`).
+2. **Deploy Strict Webview Content Security Policy (SEC-IPC-02)**:
+ - Configure `"csp": "default-src 'self'; img-src 'self' data: asset:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src ipc:; frame-src 'none'; object-src 'none'; base-uri 'none';"` in `tauri.conf.json`.
+3. **Enforce Mutual TLS & Inbound Identity Binding (NET-01)**:
+ - Require client certificates in TLS 1.3 server config and verify that inbound `ChangeHint` messages originate from the connection's authenticated `PeerId`.
+4. **Post-Extraction Manifest & Symlink Audit (EXP2-SEC-01)**:
+ - Add a recursive audit step in `install/transaction.rs` that validates all extracted files in `.local.installing` against manifest BLAKE3 digests and rejects any symbolic links or NTFS junctions.
+
+### Tier 2: Medium-Term Hardening (P1)
+*Target Timeline: Next minor version milestone*
+1. **mDNS Destination Sanitization (NET-02)**: Reject multicast, broadcast, unspecified, and zero-port endpoints in `validated_candidate_endpoint`.
+2. **Asymmetric Request Framing Bounds (NET-03)**: Reduce inbound request `FramedRead` limits to 64 KiB while maintaining 8 MiB for response snapshots.
+3. **Discovery Rate Limiter LRU Eviction (NET-04)**: Transition `RecentCandidates` from hard drop to FIFO/LRU eviction when at capacity.
+4. **Decompression Bomb Protection (EXP2-SEC-02)**: Preflight available disk space before spawning `unrar` and enforce maximum extraction size limits.
+5. **Path Validation Harmonization (EXP2-SEC-03, EXP2-SEC-04)**: Align `path_validation.rs` and `lanspread-proto` with `download/manifest.rs` (checking Windows reserved device names, NFC normalization, and trailing dots/spaces).
+6. **Thumbnail Command Sanitization (SEC-IPC-03)**: Enforce single-component validation in `get_game_thumbnail` and remove debug macros.
+7. **Unrar Subprocess Flags (SEC-IPC-04)**: Pass `-sl-` to `unrar` and verify destination containment.
+
+### Tier 3: Architectural Enhancements (P2)
+*Target Timeline: Long-term architectural refinement*
+1. **Pre-Verification Chunk Memory Buffering (EXP2-SEC-05)**: Buffer chunks in memory and verify BLAKE3 hashes prior to disk persistence.
+2. **UID-Isolated Temp State Directories (EXP2-SEC-06)**: Append process UID and enforce `0o700` permissions for fallback state paths in `/tmp`.
+3. **Capability VFS for Stream Install (EXP2-SEC-07)**: Refactor `stream_install.rs` to use `ConfinedGameRoot` handles with `FollowSymlinks::No`.
+4. **Log Viewer ReDoS Guards (SEC-FE-01)**: Enforce length limits and debouncing on frontend regex filters.
+5. **Defensive SQLite PRAGMAs (SEC-DB-01)**: Set `trusted_schema = OFF` and `cell_size_check = ON` on all SQLite connection pools.
+6. **Webview Capability Minimization (SEC-IPC-05)**: Statically configure log windows and revoke dynamic window creation capabilities.
+
+---
+
+## 7. Codebase Health & Verification Results
+
+The workspace build, test suite, and linter were executed to verify codebase integrity. All commands passed cleanly with zero regressions:
+
+### Test Suite Execution
+- **Command**: `just test`
+- **Result**: **PASS** (100% of workspace tests passing)
+- **Summary**:
+ - `lanspread-mdns`: 21 tests passed
+ - `lanspread-proto`: 25 tests passed (22 unit tests + 3 integration tests)
+ - `lanspread-tauri-deno-ts` (Rust backend): 62 tests passed (56 unit tests + 6 catalog gate tests)
+ - `lanspread-db`: all database tests passed
+ - `lanspread-peer`: all core peer tests passed
+
+### Frontend Unit Test Execution
+- **Command**: `just frontend-test`
+- **Result**: **PASS** (91 frontend reducer and state persistence tests passed in 249ms)
+
+### Clippy Linting & Static Analysis
+- **Command**: `just clippy`
+- **Result**: **PASS** (Zero warnings across all workspace crates with pedantic clippy lints enabled)
+
+---
+*Report compiled and certified for the lanspread project security audit.*
diff --git a/security-report/coverage.json b/security-report/coverage.json
new file mode 100644
index 0000000..9d6d2d6
--- /dev/null
+++ b/security-report/coverage.json
@@ -0,0 +1,572 @@
+{
+ "completeness": "partial",
+ "deferred": [
+ {
+ "candidate": {
+ "title": "Legacy index migration"
+ },
+ "candidateId": "cand-legacy-index-unbounded-migration",
+ "id": "cand-legacy-index-unbounded-migration",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "StreamInstall subprocess fanout"
+ },
+ "candidateId": "cand-stream-install-subprocess-fanout",
+ "id": "cand-stream-install-subprocess-fanout",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Path-only root generation ownership"
+ },
+ "candidateId": "cand-ownership-root-generation",
+ "id": "cand-ownership-root-generation",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Update markerless local"
+ },
+ "candidateId": "cand-update-adopts-foreign-local",
+ "id": "cand-update-adopts-foreign-local",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Uninstall markerless local"
+ },
+ "candidateId": "cand-uninstall-adopts-foreign-local",
+ "id": "cand-uninstall-adopts-foreign-local",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Windows publisher TOCTOU"
+ },
+ "candidateId": "cand-windows-package-toctou",
+ "id": "cand-windows-package-toctou",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Unbounded peer CLI input"
+ },
+ "candidateId": "cand-cli-unbounded-jsonl",
+ "id": "cand-cli-unbounded-jsonl",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Sybil peer/author slots"
+ },
+ "candidateId": "cand-sybil-peer-author-slots",
+ "id": "cand-sybil-peer-author-slots",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Main-window plugin authority"
+ },
+ "candidateId": "cand-main-window-plugin-authority",
+ "id": "cand-main-window-plugin-authority",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Application command ACL"
+ },
+ "candidateId": "cand-app-command-acl",
+ "id": "cand-app-command-acl",
+ "reason": "Pending parent validation."
+ },
+ {
+ "candidate": {
+ "title": "Discovery prefix allocation"
+ },
+ "candidateId": "cand-discovery-prefix-allocation",
+ "id": "cand-discovery-prefix-allocation",
+ "reason": "Pending parent validation."
+ }
+ ],
+ "documentType": "codex-security.coverage",
+ "excludePaths": [],
+ "explicitExclusions": [
+ {
+ "pattern": "crates/lanspread-tauri-deno-ts/src-tauri/assets/** and src-tauri/icons/**",
+ "reason": "Static image/icon payloads; file types and consumers were checked, but pixel data is not executable security logic."
+ },
+ {
+ "pattern": "crates/lanspread-peer-cli/fixtures/** and catalog fixture payload bytes (*.eti, *.ini, *.db)",
+ "reason": "Static test corpus. Publisher/loaders, catalog manifests, protocol harness, and scenario consumers were reviewed rather than treating every fixture byte as implementation source."
+ },
+ {
+ "pattern": "crates/lanspread-tauri-deno-ts/src-tauri/gen/schemas/** except ACL/capability manifests",
+ "reason": "Generated schema output; checked-in capability source and generated ACL/capability manifests owning runtime permissions were inspected."
+ },
+ {
+ "pattern": "Documentation-only design prose outside README.md and crates/lanspread-peer/ARCHITECTURE.md",
+ "reason": "Non-executable prose not owning a runtime/build control; security claims in the primary product/architecture docs were traced to consumers."
+ }
+ ],
+ "includePaths": [
+ "."
+ ],
+ "inventoryStrategy": "repository",
+ "mode": "repository",
+ "openQuestions": [
+ {
+ "followUpPrompt": "Re-run catalog provenance and manifest-size review with the retained production corpus and generated manifests.",
+ "question": "The external 186-game production package corpus and generated production manifests are absent, so their byte provenance and aggregate manifest-memory size could not be verified."
+ },
+ {
+ "followUpPrompt": "Establish vendor release/source mapping and reproducible or signed provenance for each platform sidecar.",
+ "question": "Checked-in unrar sidecars are opaque binaries with no checked-in upstream version/source/signature provenance."
+ },
+ {
+ "followUpPrompt": "Run bounded adversarial integration tests on representative Windows and physical-LAN hosts.",
+ "question": "This was an offline static scan; physical-LAN behavior, Windows UAC/runtime paths, and live memory/DOM exhaustion were not reproduced."
+ }
+ ],
+ "scanId": "916f0812-d79c-465f-a422-81d75a258f76",
+ "schemaVersion": "1.0",
+ "surfaces": [
+ {
+ "disposition": "reported",
+ "id": "surface-peer-identity-state",
+ "label": "Peer identity, authenticated endpoint generations, and aggregate remote state",
+ "notes": "Reported aggregate Sybil-state exhaustion; responder pinning, session/generation fencing, and content authority otherwise held.",
+ "receiptRefs": [],
+ "riskArea": "Identity and state resource limits"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-network-admission",
+ "label": "Anonymous QUIC admission, control frames, bulk transfers, and native StreamInstall work",
+ "notes": "Reported origin-unfair global pools/native extractor fanout and eager length-prefix allocation.",
+ "receiptRefs": [],
+ "riskArea": "Remote resource exhaustion"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-discovery-hints",
+ "label": "mDNS discovery, state hints, and liveness reconciliation",
+ "notes": "Reported pre-auth candidate monopoly and unbound third-party pull hints; pinned follow-up preserves state integrity.",
+ "receiptRefs": [],
+ "riskArea": "Discovery availability and confused deputy"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-transfer-admission",
+ "label": "Catalog transfer admission, manifest cache, ordinary downloads, and retries",
+ "notes": "Reported pre-admission manifest caching and Sybil-multiplied per-chunk deadlines; chunk/path/hash verification held.",
+ "receiptRefs": [],
+ "riskArea": "Transfer resource budgets"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-filesystem-install",
+ "label": "Download confinement, ordinary install/update/uninstall, Stream Install, and recovery",
+ "notes": "Reported uncatalogued root archive extraction. No remote path escape, chunk hash bypass, or Stream Install output bypass found.",
+ "receiptRefs": [],
+ "riskArea": "Installed-content integrity"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-call-to-play",
+ "label": "Call-to-Play protocol, author ownership, frontend reduction, and rendering",
+ "notes": "Reported cross-author EventNonce collision and maximal active-call rendering cost; creator authority and responder attribution held.",
+ "receiptRefs": [],
+ "riskArea": "UI integrity and availability"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-tauri-native",
+ "label": "Tauri commands, capabilities, storage, logs, and Windows process launch",
+ "notes": "Reported runas of mutable game scripts. No hostile-peer XSS, raw HTML/eval, arbitrary thumbnail/log read, or remote capability grant found.",
+ "receiptRefs": [],
+ "riskArea": "Native privilege boundary"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-catalog-build",
+ "label": "Catalog publisher, package traversal, just recipes, build gates, and fixture/production separation",
+ "notes": "Reported shell interpolation and bounded out-of-root preflight reads. Atomic publication and production fixture gate held.",
+ "receiptRefs": [],
+ "riskArea": "Build and publication authority"
+ },
+ {
+ "disposition": "reported",
+ "id": "surface-local-state",
+ "label": "Game-directory monitoring, migration, persisted indexes, and public per-game state helpers",
+ "notes": "Reported unbounded local ingestion/migration work and public marker path traversal.",
+ "receiptRefs": [],
+ "riskArea": "Local filesystem input"
+ },
+ {
+ "disposition": "no_issue_found",
+ "id": "surface-protocol-codecs",
+ "label": "Protocol-v8 codecs, canonical IDs/paths, strict JSON, and legacy rejection",
+ "notes": "Strict current-only shapes, canonical path/ID types, unknown-field rejection, and semantic domain validation were traced; the aggregate decoder allocation issue is separately reported.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "no_issue_found",
+ "id": "surface-identity-secrets",
+ "label": "Installation key persistence and secret handling",
+ "notes": "Bounded strict identity parsing, certificate/key/SPKI/SAN consistency, no-follow/no-clobber persistence, Unix private mode, and redacted diagnostics were verified.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "no_issue_found",
+ "id": "surface-lifecycle",
+ "label": "Cancellation, task/process ownership, generation shutdown, and crash recovery",
+ "notes": "Owned child tasks, process reaping, network admission closure, operation drain, version/install intents, and quarantine paths were traced with no detached product mutation path found.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "no_issue_found",
+ "id": "surface-frontend-injection",
+ "label": "Frontend rendering, URLs, CSS, async ownership, and auxiliary windows",
+ "notes": "Remote values are React-escaped and no raw HTML, eval, javascript URL, or remote navigation sink was found; CSP null and broad app permissions remain defense-in-depth concerns.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "no_issue_found",
+ "id": "surface-test-tooling",
+ "label": "Peer CLI, Docker/just/Python scenario harnesses, and integration tests",
+ "notes": "Developer-authority boundaries, argv construction, cleanup scopes, fixture separation, and async lifecycle tests were reviewed; local stdin self-exhaustion was not treated as a security boundary.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-discovery-prefix-allocation",
+ "label": "Discovery length-prefix allocation candidate",
+ "notes": "Merged into `resource-exhaustion.length-prefix-eager-reserve`; same locked decoder, aggregate-memory failure, evidence, and remediation. Evidence: discovery.rs:30-259; network.rs:98-215.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-app-command-acl",
+ "label": "Missing application-command ACL candidate",
+ "notes": "No source-backed attacker-controlled script execution exists in either fixed local log window; fixed local URLs and React escaping are counterevidence. Retained as least-privilege hardening.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-main-window-plugin-authority",
+ "label": "Broad main-window plugin authority candidate",
+ "notes": "Requires a compromised main renderer, already intentionally trusted with native custom commands; no current injection/navigation route establishes the prerequisite.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-sybil-peer-author-slots",
+ "label": "Persistent Sybil peer/author slots candidate",
+ "notes": "Merged into `resource-exhaustion.sybil-aggregate-state`; same identity multiplication, retained-state budget, unbounded publication, and remediation.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-cli-unbounded-jsonl",
+ "label": "Unbounded peer CLI input candidate",
+ "notes": "Local stdin controller already owns the developer harness and can block/terminate it; no lower-privilege or remote boundary is crossed. Evidence: peer-cli/main.rs:167-180,409-428.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-windows-package-toctou",
+ "label": "Windows publisher TOCTOU candidate",
+ "notes": "Merged into `path-traversal.catalog-preflight-links`; same path-based read/rooted-handle failure and remediation. Evidence: catalog_publisher/package.rs:352-425.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-uninstall-adopts-foreign-local",
+ "label": "Markerless local uninstall candidate",
+ "notes": "Explicit authorized uninstall semantics: `local/` defines installed state and user-invoked uninstall intentionally removes it; marker creation proves the renamed backup is transaction-owned, not prior install provenance.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-update-adopts-foreign-local",
+ "label": "Markerless local update candidate",
+ "notes": "Explicit authorized update semantics: update intentionally replaces existing `local/`, preserves it as backup until successful promotion, and restores on failure.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-ownership-root-generation",
+ "label": "Path-only ownership generation candidate",
+ "notes": "No meaningful attacker capability gain: an actor able to replace the entire game root already controls those files, and source tests intentionally preserve path ownership across recreation of the same configured root.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-stream-install-subprocess-fanout",
+ "label": "StreamInstall subprocess fanout candidate",
+ "notes": "Merged into `resource-exhaustion.anonymous-network-pools` as the expensive bulk operation enabled by the same absent per-origin/costly-work quota.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "cand-legacy-index-unbounded-migration",
+ "label": "Legacy library-index migration candidate",
+ "notes": "Merged into `resource-exhaustion.local-library-ingestion`; same selected-filesystem byte/cardinality budget gap and remediation.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-sybil-aggregate-state",
+ "label": "Sybil peers can exhaust aggregate state and unbounded UI publication",
+ "notes": "Validated finding 1.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-anonymous-global-pool-dos",
+ "label": "Anonymous LAN requesters can monopolize global pools and native extractors",
+ "notes": "Validated finding 2.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-manifest-cache-before-admission",
+ "label": "Rejected bulk requests can populate the persistent manifest cache",
+ "notes": "Validated finding 3.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-inbound-prefix-allocation",
+ "label": "Control-frame prefixes can reserve about 512 MiB across concurrent decoders",
+ "notes": "Validated finding 4.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-elevated-mutable-game-scripts",
+ "label": "Mutable game scripts are launched elevated without launch-time trust binding",
+ "notes": "Validated finding 5.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-cross-author-event-collision",
+ "label": "Cross-author event-ID collisions can suppress Call-to-Play entries",
+ "notes": "Validated finding 6.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-unauthenticated-hint-proxy",
+ "label": "Unauthenticated hints can make the victim pull arbitrary known peers",
+ "notes": "Validated finding 7.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-mdns-discovery-monopoly",
+ "label": "Forged mDNS candidates can monopolize discovery slots",
+ "notes": "Validated finding 8.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-local-library-budget",
+ "label": "Selected game directories can trigger unbounded monitoring and migration work",
+ "notes": "Validated finding 9.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-justfile-shell-interpolation",
+ "label": "Catalog/build recipe arguments are interpolated as shell code",
+ "notes": "Validated finding 10.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-package-root-link-preflight",
+ "label": "Catalog preflight can read outside the package root through links and Windows races",
+ "notes": "Validated finding 11.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-ordinary-install-uncatalogued-eti",
+ "label": "Ordinary install extracts uncatalogued root archives without output verification",
+ "notes": "Validated finding 12.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-discovery-prefix-allocation",
+ "label": "Discovery prefix allocation",
+ "notes": "Merged into the reportable length-prefix finding: same locked decoder, aggregate-memory failure, and remediation; the discovery path is preserved as an affected entry point.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-app-command-acl",
+ "label": "Application command ACL",
+ "notes": "Not separately reportable: no source-backed attacker-controlled script execution exists in either fixed local log window; React text rendering and fixed URLs are counterevidence. Retained as least-privilege hardening.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-main-window-plugin-authority",
+ "label": "Main-window plugin authority",
+ "notes": "Not separately reportable: it requires a compromised main renderer, for which the application intentionally exposes a broad native command surface; no current injection/navigation route establishes that prerequisite.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-sybil-peer-author-slots",
+ "label": "Sybil peer/author slots",
+ "notes": "Merged into the reportable aggregate Sybil-state finding: same first-come identity admission, retained-state budget, unbounded full-view publication, and remediation.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-cli-unbounded-jsonl",
+ "label": "Unbounded peer CLI input",
+ "notes": "Rejected as self-only developer-harness behavior: the local stdin controller already owns the harness and can terminate or block it; no lower-privilege or remote boundary is crossed.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-windows-package-toctou",
+ "label": "Windows publisher TOCTOU",
+ "notes": "Merged into the publisher link/TOCTOU finding because both arise from path-based reads without rooted no-follow identity and share the same remediation.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-uninstall-adopts-foreign-local",
+ "label": "Uninstall markerless local",
+ "notes": "Rejected as explicit authorized uninstall semantics. `local/` defines installed state and the user-invoked uninstall intentionally removes it; `.lanspread_owned` is added so the renamed backup is transaction-owned, not as prior install provenance.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-update-adopts-foreign-local",
+ "label": "Update markerless local",
+ "notes": "Rejected as explicit authorized update semantics. A user-invoked update intentionally replaces an existing `local/` tree while preserving it as backup until promotion succeeds.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-ownership-root-generation",
+ "label": "Path-only root generation ownership",
+ "notes": "Rejected for lack of meaningful attacker capability gain: a same-authority actor able to replace the whole game root already controls those files, and tests/documentation intentionally preserve path ownership across recreation of the same configured root.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-stream-install-subprocess-fanout",
+ "label": "StreamInstall subprocess fanout",
+ "notes": "Merged into the anonymous global-pools finding as the expensive bulk operation enabled by the same missing per-origin/global costly-work quota.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "rejected",
+ "id": "validation-cand-legacy-index-unbounded-migration",
+ "label": "Legacy index migration",
+ "notes": "Merged into the local-library ingestion finding because it is another whole-file/cardinality budget gap on the same selected-filesystem boundary.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "baseline-general-audit",
+ "label": "Independent baseline audit",
+ "notes": "Two candidates.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "network-tls-protocol",
+ "label": "Network, TLS, protocol, and transfer admission",
+ "notes": "Three candidates.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "tauri-frontend-native",
+ "label": "Tauri commands, frontend, windows, and process launch",
+ "notes": "Three candidates.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "peer-state-business-logic",
+ "label": "Discovery, liveness, peer state, and Call-to-Play",
+ "notes": "Four candidates.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "cli-local-runtime",
+ "label": "Peer CLI, local library monitor, and runtime glue",
+ "notes": "Two candidates.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "catalog-build-publication",
+ "label": "Catalog publisher, build gates, and package traversal",
+ "notes": "Three candidates.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "filesystem-install-recovery",
+ "label": "Download confinement, install/update/uninstall, streamed install, and recovery",
+ "notes": "Seven candidates.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-ctp-active-call-rendering",
+ "label": "One peer can force continuous rendering of thousands of active calls",
+ "notes": "Validated residual frontend finding.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-sybil-download-retry-budget",
+ "label": "Sybil sources can multiply one chunk deadline into a multi-hour retry loop",
+ "notes": "Validated residual download finding.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "residual-download-coverage",
+ "label": "Residual download planning, retry, progress, and drain modules",
+ "notes": "Retry-budget candidate remains pending.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "residual-frontend-coverage",
+ "label": "Residual production frontend components and reducers",
+ "notes": "One semantic rendering-budget candidate checkpointed before validation.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "reported",
+ "id": "reported-cand-public-state-path-traversal",
+ "label": "Public state helpers permit marker writes outside the state directory",
+ "notes": "Validated public API path-traversal finding.",
+ "receiptRefs": []
+ },
+ {
+ "disposition": "needs_follow_up",
+ "id": "residual-peer-core-coverage",
+ "label": "Residual peer-core lifecycle, state paths, quarantine, startup, and fa\u00e7ades",
+ "notes": "One public state-path candidate checkpointed before validation.",
+ "receiptRefs": []
+ }
+ ]
+}
diff --git a/security-report/exports/results.sarif b/security-report/exports/results.sarif
new file mode 100644
index 0000000..5bbaf54
--- /dev/null
+++ b/security-report/exports/results.sarif
@@ -0,0 +1,1614 @@
+{
+ "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json",
+ "runs": [
+ {
+ "automationDetails": {
+ "id": "916f0812-d79c-465f-a422-81d75a258f76"
+ },
+ "invocations": [
+ {
+ "executionSuccessful": true,
+ "toolExecutionNotifications": [
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ },
+ {
+ "level": "warning",
+ "message": {
+ "text": "Pending parent validation."
+ }
+ }
+ ]
+ }
+ ],
+ "properties": {
+ "codexSecurityCoverageCompleteness": "partial",
+ "codexSecuritySchemaVersion": "1.0",
+ "codexSecurityTargetKind": "git_revision"
+ },
+ "results": [
+ {
+ "level": "note",
+ "locations": [
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/local_monitor.rs"
+ },
+ "region": {
+ "endLine": 235,
+ "startLine": 200
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/local_monitor.rs"
+ },
+ "region": {
+ "endLine": 376,
+ "startLine": 329
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/local_games.rs"
+ },
+ "region": {
+ "endLine": 294,
+ "startLine": 268
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/migration.rs"
+ },
+ "region": {
+ "endLine": 297,
+ "startLine": 284
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:local-index-whole-read"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/local_games.rs"
+ },
+ "region": {
+ "endLine": 284,
+ "startLine": 268
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Selected game directories can trigger unbounded monitoring and migration work\n\nThe continuously monitored game root retains every top-level name, spawns one task per changed ID, recursively walks trees, and reads current/legacy state files without byte budgets.\n\nSeverity: low\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nIntersect names with the catalog before retention/task admission; add root/per-game/depth/metadata budgets and a bounded rescan pool; bounded-read current and legacy indexes/version.ini and avoid raw invalid-content logging."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:57f045ce125ace9a603ada98f96c92eb5c248a29cbe75d763ea9ee8beaccc919"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_b4156302e37bcb0e5a87f4af",
+ "occurrenceId": "occ_396ac8c331a26fedd06ad199",
+ "severity": "low"
+ },
+ "ruleId": "resource-exhaustion.local-library-ingestion",
+ "ruleIndex": 9
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/tls.rs"
+ },
+ "region": {
+ "endLine": 133,
+ "startLine": 126
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/server.rs"
+ },
+ "region": {
+ "endLine": 44,
+ "startLine": 31
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/stream.rs"
+ },
+ "region": {
+ "endLine": 89,
+ "startLine": 70
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/stream_install.rs"
+ },
+ "region": {
+ "endLine": 414,
+ "startLine": 387
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Anonymous LAN requesters can monopolize global pools and native extractors\n\nThe responder authenticates no client and applies only global connection, control, and bulk limits, so one origin can occupy every admission slot and drive up to 48 concurrent StreamInstall operations with native unrar work.\n\nSeverity: medium\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nAdd per-source connection/stream/rate quotas, close origins after repeated timeouts, reserve capacity across origins, place unrar behind a small global and per-origin semaphore, and enforce absolute transfer deadlines."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:1d70a00f495ad3463b91c2fcf08d4d7fcd7d3ff00cf1fc63b4a96b46ba0008b7"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_984da905a9b4074194d1814b",
+ "occurrenceId": "occ_4b80e3b7dfe1e6768d05ba94",
+ "severity": "medium"
+ },
+ "ruleId": "resource-exhaustion.anonymous-network-pools",
+ "ruleIndex": 5
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/transfer.rs"
+ },
+ "region": {
+ "endLine": 321,
+ "startLine": 258
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-db/src/content_manifest/store.rs"
+ },
+ "region": {
+ "endLine": 186,
+ "startLine": 162
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:cache-load-before-check"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/transfer.rs"
+ },
+ "region": {
+ "endLine": 284,
+ "startLine": 270
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:cache-stream-before-check"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/transfer.rs"
+ },
+ "region": {
+ "endLine": 319,
+ "startLine": 308
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Rejected bulk requests can populate the persistent manifest cache\n\nBoth bulk request variants load and retain a full catalog manifest before checking compact ContentId equality or whether the game is locally serveable.\n\nSeverity: medium\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nCheck `content_identity` and local readiness first, then require `cached_manifest`; add tests proving rejected requests perform no load, plus an aggregate cache budget."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:c71b31921c2ee1ae38de77962c2bfd27469df63611c19a80e083c349ee24daca"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_28dcda78d4a8e1d179a20e6f",
+ "occurrenceId": "occ_4c4e343d108a709a37ed2015",
+ "severity": "medium"
+ },
+ "ruleId": "resource-exhaustion.manifest-cache-admission-order",
+ "ruleIndex": 10
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/install/transaction.rs"
+ },
+ "region": {
+ "endLine": 562,
+ "startLine": 449
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs"
+ },
+ "region": {
+ "endLine": 3206,
+ "startLine": 3195
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/handlers.rs"
+ },
+ "region": {
+ "endLine": 1488,
+ "startLine": 1443
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:eti-all-root-files"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/install/transaction.rs"
+ },
+ "region": {
+ "endLine": 562,
+ "startLine": 514
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:eti-promote-unverified"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/install/transaction.rs"
+ },
+ "region": {
+ "endLine": 468,
+ "startLine": 449
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Ordinary install extracts uncatalogued root archives without output verification\n\nInstall/update enumerates every root regular `.eti` file\u2014including unknown files deliberately preserved beside downloads\u2014runs native extraction for each, and promotes the staging tree without comparing the archive set or extracted output to the local catalog.\n\nSeverity: medium\n\nCategory: Content integrity\n\nWeaknesses: CWE-494, CWE-400\n\nRemediation:\nPass the exact catalog manifest into install/update; require the exact root archive set and verify each archive size/BLAKE3 through no-follow handles; verify every extracted path/kind/size/digest before promotion; enforce entry/decompressed-byte/total deadlines."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:effedb3e055f82da4175c26119809a0af75499bf7de3c2418c62b0b4ef3e0777"
+ },
+ "properties": {
+ "category": "content-integrity",
+ "confidence": "high",
+ "findingId": "csf_2e096654b094e0fac1b124cd",
+ "occurrenceId": "occ_4db1454c427333bbd2c39100",
+ "severity": "medium"
+ },
+ "ruleId": "integrity.ordinary-install-uncatalogued-archive",
+ "ruleIndex": 1
+ },
+ {
+ "level": "note",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/state_paths.rs"
+ },
+ "region": {
+ "endLine": 59,
+ "startLine": 44
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/launch_settings.rs"
+ },
+ "region": {
+ "endLine": 175,
+ "startLine": 168
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/launch_settings.rs"
+ },
+ "region": {
+ "endLine": 389,
+ "startLine": 383
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Public state helpers permit marker writes outside the state directory\n\nPublic per-game path and marker APIs join an unvalidated game ID; absolute or parent-containing IDs escape `state_dir/games`, after which the marker writer creates parents and overwrites a fixed file.\n\nSeverity: low\n\nCategory: Path traversal\n\nWeaknesses: CWE-22\n\nRemediation:\nRequire a validated single-component GameId newtype or make helpers fallible and reject absolute/prefix/dot/parent/separator/reserved forms; write relative to a retained no-follow state-directory handle."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:0a3fdc1b7078165af877d4c58e0c2168c9f3911c3ae9a5086ef452dda147be52"
+ },
+ "properties": {
+ "category": "path-traversal",
+ "confidence": "high",
+ "findingId": "csf_8fefa5cb607eaa71c76a60ae",
+ "occurrenceId": "occ_7a4292dcad47ad7e3e8412ef",
+ "severity": "low"
+ },
+ "ruleId": "path-traversal.public-state-path-helper",
+ "ruleIndex": 3
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/download/retry.rs"
+ },
+ "region": {
+ "endLine": 327,
+ "startLine": 294
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/download/transport.rs"
+ },
+ "region": {
+ "endLine": 34,
+ "startLine": 34
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "source"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/peer_db.rs"
+ },
+ "region": {
+ "endLine": 667,
+ "startLine": 654
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:retry-transport-requeues"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/download/retry.rs"
+ },
+ "region": {
+ "endLine": 219,
+ "startLine": 192
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Sybil sources can multiply one chunk deadline into a multi-hour retry loop\n\nA download retries every distinct peer identity with no numeric or total deadline; transport stalls remain retryable and each of up to 64 Sybil sources receives a fresh ten-minute chunk deadline.\n\nSeverity: medium\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nAdd total per-chunk/download wall-clock and attempt budgets independent of source count, cap automatically tried identities, add per-origin identity quotas, and require explicit user retry after budget exhaustion."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:73147d89e16ed49c48a790b8e6bb11fd0b5d7733f568fca8b60abb87a597f57a"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_1c204082cf65f3b51769f582",
+ "occurrenceId": "occ_80bdddc3df1afd907ea7579a",
+ "severity": "medium"
+ },
+ "ruleId": "resource-exhaustion.download-retry-total-budget",
+ "ruleIndex": 7
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-proto/src/lib.rs"
+ },
+ "region": {
+ "endLine": 20,
+ "startLine": 17
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src/hooks/useCallToPlay.ts"
+ },
+ "region": {
+ "endLine": 72,
+ "startLine": 68
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx"
+ },
+ "region": {
+ "endLine": 154,
+ "startLine": 80
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:ctp-render-all-rows"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx"
+ },
+ "region": {
+ "endLine": 94,
+ "startLine": 80
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "One peer can force continuous rendering of thousands of active calls\n\nA valid 4,096-event author slice can contain thousands of simultaneous Create roots; the frontend reduces and sorts the full set every second and renders every nomination in the always-mounted ticker.\n\nSeverity: medium\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nEnforce small per-author/global active-call caps before publication, render only a ranked ticker subset, virtualize/paginate the overlay, and avoid recomputing unchanged projections on every clock tick."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:0720a2a98628af9fcddc5bcbaffb29db6a8dbaef7bf4b32bdd6eb76635d7ced9"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_a094c3abe43e8a361f0e7f84",
+ "occurrenceId": "occ_83d9017253563f8852490859",
+ "severity": "medium"
+ },
+ "ruleId": "resource-exhaustion.call-to-play-rendering",
+ "ruleIndex": 6
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-proto/src/lib.rs"
+ },
+ "region": {
+ "endLine": 20,
+ "startLine": 13
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/server.rs"
+ },
+ "region": {
+ "endLine": 41,
+ "startLine": 31
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/stream.rs"
+ },
+ "region": {
+ "endLine": 67,
+ "startLine": 37
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/discovery.rs"
+ },
+ "region": {
+ "endLine": 32,
+ "startLine": 30
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "Cargo.lock"
+ },
+ "region": {
+ "endLine": 4659,
+ "startLine": 4655
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:prefix-frame-max"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-proto/src/lib.rs"
+ },
+ "region": {
+ "endLine": 14,
+ "startLine": 13
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Control-frame prefixes can reserve about 512 MiB across concurrent decoders\n\nThe same 8 MiB control-frame allowance is multiplied across 64 concurrent inbound request decoders and 64 discovery Hello decoders; locked tokio-util eagerly reserves the declared body after only the length prefix.\n\nSeverity: medium\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nUse a much smaller inbound Request-frame maximum; gate declared response bytes through a global memory semaphore or incremental buffering; reduce discovery fan-out as defense in depth."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:fffdefc91a869c1291f679a54297d18c889dc2e049b391e4534d0a53e61769cf"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_d40a863e76a3bf4cb0bd412a",
+ "occurrenceId": "occ_86bfac07819428f756982d0e",
+ "severity": "medium"
+ },
+ "ruleId": "resource-exhaustion.length-prefix-eager-reserve",
+ "ruleIndex": 8
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "justfile"
+ },
+ "region": {
+ "endLine": 49,
+ "startLine": 22
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "justfile"
+ },
+ "region": {
+ "endLine": 118,
+ "startLine": 105
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "justfile"
+ },
+ "region": {
+ "endLine": 170,
+ "startLine": 140
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:just-raw-games-dir"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "justfile"
+ },
+ "region": {
+ "endLine": 31,
+ "startLine": 22
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Catalog/build recipe arguments are interpolated as shell code\n\nDocumented just recipes embed path, selector, output, and environment-derived values directly into shell source; command substitution and quote-breaking run before Rust/Python argv parsing.\n\nSeverity: medium\n\nCategory: Command injection\n\nWeaknesses: CWE-78\n\nRemediation:\nPass data through environment/argv to non-shell helpers or apply just's correct shell-quoting facility to every argument and environment-derived value; test `$()`, quotes, whitespace, leading dashes, and newlines."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:612aaf874e672716f24d57db2b8b5793bf29242580683c9009791017da66b81c"
+ },
+ "properties": {
+ "category": "command-injection",
+ "confidence": "high",
+ "findingId": "csf_52309c6d49d62ae81258872c",
+ "occurrenceId": "occ_9293ad354e62522b50537a56",
+ "severity": "medium"
+ },
+ "ruleId": "command-injection.justfile-interpolation",
+ "ruleIndex": 0
+ },
+ {
+ "level": "note",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-compat/src/catalog_publisher/mod.rs"
+ },
+ "region": {
+ "endLine": 130,
+ "startLine": 120
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-compat/src/catalog_publisher/package.rs"
+ },
+ "region": {
+ "endLine": 137,
+ "startLine": 125
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-compat/src/catalog_publisher/package.rs"
+ },
+ "region": {
+ "endLine": 425,
+ "startLine": 403
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Catalog preflight can read outside the package root through links and Windows races\n\nVersion preflight joins `package_root/version.ini` before validating the game root, and Windows disables file-identity comparison, allowing ancestor-link traversal or a raced reparse target to be read and echoed.\n\nSeverity: low\n\nCategory: Path traversal\n\nWeaknesses: CWE-59, CWE-367\n\nRemediation:\nOpen the packages/game roots through retained no-follow handles and open `version.ini` relative to them; implement Windows file-ID/reparse-safe identity checks; never print raw untrusted version contents."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:a4df8ff5972c151be57355357b646ab9cd4276992c8021cc8257d35d60a9f80a"
+ },
+ "properties": {
+ "category": "path-traversal",
+ "confidence": "high",
+ "findingId": "csf_5079213437e3f254ae70f2ca",
+ "occurrenceId": "occ_97c865ee39aa6ae912efe24f",
+ "severity": "low"
+ },
+ "ruleId": "path-traversal.catalog-preflight-links",
+ "ruleIndex": 2
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/call_to_play.rs"
+ },
+ "region": {
+ "endLine": 565,
+ "startLine": 555
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/events.rs"
+ },
+ "region": {
+ "endLine": 25,
+ "startLine": 21
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/peer_db.rs"
+ },
+ "region": {
+ "endLine": 360,
+ "startLine": 356
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "source"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-proto/src/lib.rs"
+ },
+ "region": {
+ "endLine": 20,
+ "startLine": 15
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:sybil-full-projection"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/call_to_play.rs"
+ },
+ "region": {
+ "endLine": 855,
+ "startLine": 835
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Sybil peers can exhaust aggregate state and unbounded UI publication\n\nOne LAN host can generate enough valid peer identities to fill peer/author slots, retain hundreds of MiB of bounded-per-author state, and repeatedly enqueue cloned full views without an aggregate byte budget.\n\nSeverity: medium\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nEnforce aggregate byte/event budgets across remote peers, coalesce complete UI state through bounded latest-value channels, avoid full cloning per commit, and add fair/per-origin admission for identities."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:98060f19cf12dd314a2039a9dc1eb93158ac43200f525155125360ac5f7b7a9c"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_7f1f254725189aac18ef1e44",
+ "occurrenceId": "occ_b1ba111f96d0b687d00e5778",
+ "severity": "medium"
+ },
+ "ruleId": "resource-exhaustion.sybil-aggregate-state",
+ "ruleIndex": 12
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/state_sync.rs"
+ },
+ "region": {
+ "endLine": 388,
+ "startLine": 351
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/stream.rs"
+ },
+ "region": {
+ "endLine": 271,
+ "startLine": 265
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/remote_state.rs"
+ },
+ "region": {
+ "endLine": 159,
+ "startLine": 147
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Unauthenticated hints can make the victim pull arbitrary known peers\n\nAnonymous inbound connections may name any known PeerId in a change hint; a forged session/revision mismatch schedules a full pinned Hello pull to that unrelated peer.\n\nSeverity: medium\n\nCategory: Confused deputy\n\nWeaknesses: CWE-441\n\nRemediation:\nIf requester authentication remains absent, drop remote change hints and rely on pinned liveness reconciliation. Otherwise bind the hint to an authenticated client PeerId and remove the payload identity."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:c463c38e3d5499ce06d5279a12064947fd41f36deea0e7c6514b71d8077724a2"
+ },
+ "properties": {
+ "category": "confused-deputy",
+ "confidence": "high",
+ "findingId": "csf_f3f28e25fca367bd97e7cdac",
+ "occurrenceId": "occ_dc1e0812adfa054e42cddf8e",
+ "severity": "medium"
+ },
+ "ruleId": "resource-exhaustion.unauthenticated-state-hint-proxy",
+ "ruleIndex": 13
+ },
+ {
+ "level": "warning",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs"
+ },
+ "region": {
+ "endLine": 1582,
+ "startLine": 1554
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs"
+ },
+ "region": {
+ "endLine": 1958,
+ "startLine": 1866
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs"
+ },
+ "region": {
+ "endLine": 2064,
+ "startLine": 2007
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:runas-game-path"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs"
+ },
+ "region": {
+ "endLine": 1951,
+ "startLine": 1891
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:runas-server-path"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs"
+ },
+ "region": {
+ "endLine": 2057,
+ "startLine": 2026
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:runas-verb"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs"
+ },
+ "region": {
+ "endLine": 1577,
+ "startLine": 1563
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Mutable game scripts are launched elevated without launch-time trust binding\n\nWindows play/server commands follow mutable paths under any selected game root and pass setup/start scripts to `cmd.exe` with `runas` without rechecking catalog membership, digest, installed ownership, or reparse-free containment.\n\nSeverity: medium\n\nCategory: Privilege escalation\n\nWeaknesses: CWE-250, CWE-73\n\nRemediation:\nRun ordinary game/server scripts as the current user. For setup requiring elevation, require a catalog game and authoritative installed state, verify the exact script digest immediately before launch through no-follow handles, reject reparse points, and preserve the verified object identity into process creation."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:2d051e05bbf21a3a948ad8f9028519a29bd04980ea7c981575c9c956de7a0e20"
+ },
+ "properties": {
+ "category": "privilege-escalation",
+ "confidence": "high",
+ "findingId": "csf_440f1250535a69bcbbeb8d14",
+ "occurrenceId": "occ_e3fcb8021db0f8fa3a50cdad",
+ "severity": "medium"
+ },
+ "ruleId": "privilege-escalation.mutable-game-scripts",
+ "ruleIndex": 4
+ },
+ {
+ "level": "note",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/call_to_play.rs"
+ },
+ "region": {
+ "endLine": 979,
+ "startLine": 961
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts"
+ },
+ "region": {
+ "endLine": 145,
+ "startLine": 135
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:ctp-author-preserved"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/call_to_play.rs"
+ },
+ "region": {
+ "endLine": 884,
+ "startLine": 877
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Cross-author event-ID collisions can suppress Call-to-Play entries\n\nRust validates EventNonce uniqueness only within each authenticated author's slice, but the frontend globally deduplicates events by nonce alone, letting a hostile author overwrite another author's event in the rendered view.\n\nSeverity: low\n\nCategory: State integrity\n\nWeaknesses: CWE-694\n\nRemediation:\nUse `(author_id, event.id)` as the key for deduplication, messages, React keys, and ordering; add cross-author collision tests including collision with a creator's Create event."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:453e4b66813024523868694a6ea51b2d7948c85ceda20110a895465e3d26f7ae"
+ },
+ "properties": {
+ "category": "state-integrity",
+ "confidence": "high",
+ "findingId": "csf_40c94234ad7f1c26e592c9b8",
+ "occurrenceId": "occ_e884be7f0807f9071e8f54ec",
+ "severity": "low"
+ },
+ "ruleId": "state-integrity.call-to-play-event-id-collision",
+ "ruleIndex": 14
+ },
+ {
+ "level": "note",
+ "locations": [
+ {
+ "message": {
+ "text": "root_control"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/discovery.rs"
+ },
+ "region": {
+ "endLine": 50,
+ "startLine": 30
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "entrypoint"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/discovery.rs"
+ },
+ "region": {
+ "endLine": 259,
+ "startLine": 233
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "sink"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/discovery.rs"
+ },
+ "region": {
+ "endLine": 309,
+ "startLine": 294
+ }
+ }
+ },
+ {
+ "message": {
+ "text": "evidence:mdns-full-drop"
+ },
+ "physicalLocation": {
+ "artifactLocation": {
+ "uri": "crates/lanspread-peer/src/services/discovery.rs"
+ },
+ "region": {
+ "endLine": 309,
+ "startLine": 300
+ }
+ }
+ }
+ ],
+ "message": {
+ "text": "Forged mDNS candidates can monopolize discovery slots\n\nSixty-four unique attacker-controlled PeerId/address hints occupy every active and recent candidate slot before TLS proof, causing legitimate new candidates to be dropped while the attacker rotates records.\n\nSeverity: low\n\nCategory: Resource exhaustion\n\nWeaknesses: CWE-400\n\nRemediation:\nAdd per-source-IP quotas, preserve capacity across origins/known peers, and use fair/randomized replacement or a cheap proof-of-key before the longer negotiation slot."
+ },
+ "partialFingerprints": {
+ "codexSecurity/v1": "codex-security/v1:sha256:3354152dfaef2ebe80ded38bcc4d7469856a92511d005d07d97fb1fe3046376b"
+ },
+ "properties": {
+ "category": "resource-exhaustion",
+ "confidence": "high",
+ "findingId": "csf_a8fd48f15b36d21c1eae743b",
+ "occurrenceId": "occ_f3586624a8d633db89a319d4",
+ "severity": "low"
+ },
+ "ruleId": "resource-exhaustion.mdns-candidate-monopoly",
+ "ruleIndex": 11
+ }
+ ],
+ "tool": {
+ "driver": {
+ "name": "Codex Security",
+ "rules": [
+ {
+ "fullDescription": {
+ "text": "Command injection: Justfile interpolation. Categories: Command injection. Weaknesses: CWE-78."
+ },
+ "help": {
+ "markdown": "Command injection: Justfile interpolation. Categories: Command injection. Weaknesses: CWE-78.\n\n## Remediation\n\nPass data through environment/argv to non-shell helpers or apply just's correct shell-quoting facility to every argument and environment-derived value; test `$()`, quotes, whitespace, leading dashes, and newlines.",
+ "text": "Command injection: Justfile interpolation. Categories: Command injection. Weaknesses: CWE-78.\n\nRemediation:\n\nPass data through environment/argv to non-shell helpers or apply just's correct shell-quoting facility to every argument and environment-derived value; test `$()`, quotes, whitespace, leading dashes, and newlines."
+ },
+ "id": "command-injection.justfile-interpolation",
+ "name": "Command injection: Justfile interpolation",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "command-injection",
+ "external/cwe/cwe-078",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Command injection: Justfile interpolation"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Integrity: Ordinary install uncatalogued archive. Categories: Content integrity. Weaknesses: CWE-400, CWE-494."
+ },
+ "help": {
+ "markdown": "Integrity: Ordinary install uncatalogued archive. Categories: Content integrity. Weaknesses: CWE-400, CWE-494.\n\n## Remediation\n\nPass the exact catalog manifest into install/update; require the exact root archive set and verify each archive size/BLAKE3 through no-follow handles; verify every extracted path/kind/size/digest before promotion; enforce entry/decompressed-byte/total deadlines.",
+ "text": "Integrity: Ordinary install uncatalogued archive. Categories: Content integrity. Weaknesses: CWE-400, CWE-494.\n\nRemediation:\n\nPass the exact catalog manifest into install/update; require the exact root archive set and verify each archive size/BLAKE3 through no-follow handles; verify every extracted path/kind/size/digest before promotion; enforce entry/decompressed-byte/total deadlines."
+ },
+ "id": "integrity.ordinary-install-uncatalogued-archive",
+ "name": "Integrity: Ordinary install uncatalogued archive",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "content-integrity",
+ "external/cwe/cwe-400",
+ "external/cwe/cwe-494",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Integrity: Ordinary install uncatalogued archive"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Path traversal: Catalog preflight links. Categories: Path traversal. Weaknesses: CWE-367, CWE-59."
+ },
+ "help": {
+ "markdown": "Path traversal: Catalog preflight links. Categories: Path traversal. Weaknesses: CWE-367, CWE-59.\n\n## Remediation\n\nOpen the packages/game roots through retained no-follow handles and open `version.ini` relative to them; implement Windows file-ID/reparse-safe identity checks; never print raw untrusted version contents.",
+ "text": "Path traversal: Catalog preflight links. Categories: Path traversal. Weaknesses: CWE-367, CWE-59.\n\nRemediation:\n\nOpen the packages/game roots through retained no-follow handles and open `version.ini` relative to them; implement Windows file-ID/reparse-safe identity checks; never print raw untrusted version contents."
+ },
+ "id": "path-traversal.catalog-preflight-links",
+ "name": "Path traversal: Catalog preflight links",
+ "properties": {
+ "security-severity": "2.0",
+ "tags": [
+ "external/cwe/cwe-059",
+ "external/cwe/cwe-367",
+ "path-traversal",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Path traversal: Catalog preflight links"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Path traversal: Public state path helper. Categories: Path traversal. Weaknesses: CWE-22."
+ },
+ "help": {
+ "markdown": "Path traversal: Public state path helper. Categories: Path traversal. Weaknesses: CWE-22.\n\n## Remediation\n\nRequire a validated single-component GameId newtype or make helpers fallible and reject absolute/prefix/dot/parent/separator/reserved forms; write relative to a retained no-follow state-directory handle.",
+ "text": "Path traversal: Public state path helper. Categories: Path traversal. Weaknesses: CWE-22.\n\nRemediation:\n\nRequire a validated single-component GameId newtype or make helpers fallible and reject absolute/prefix/dot/parent/separator/reserved forms; write relative to a retained no-follow state-directory handle."
+ },
+ "id": "path-traversal.public-state-path-helper",
+ "name": "Path traversal: Public state path helper",
+ "properties": {
+ "security-severity": "2.0",
+ "tags": [
+ "external/cwe/cwe-022",
+ "path-traversal",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Path traversal: Public state path helper"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Privilege escalation: Mutable game scripts. Categories: Privilege escalation. Weaknesses: CWE-250, CWE-73."
+ },
+ "help": {
+ "markdown": "Privilege escalation: Mutable game scripts. Categories: Privilege escalation. Weaknesses: CWE-250, CWE-73.\n\n## Remediation\n\nRun ordinary game/server scripts as the current user. For setup requiring elevation, require a catalog game and authoritative installed state, verify the exact script digest immediately before launch through no-follow handles, reject reparse points, and preserve the verified object identity into process creation.",
+ "text": "Privilege escalation: Mutable game scripts. Categories: Privilege escalation. Weaknesses: CWE-250, CWE-73.\n\nRemediation:\n\nRun ordinary game/server scripts as the current user. For setup requiring elevation, require a catalog game and authoritative installed state, verify the exact script digest immediately before launch through no-follow handles, reject reparse points, and preserve the verified object identity into process creation."
+ },
+ "id": "privilege-escalation.mutable-game-scripts",
+ "name": "Privilege escalation: Mutable game scripts",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "external/cwe/cwe-073",
+ "external/cwe/cwe-250",
+ "privilege-escalation",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Privilege escalation: Mutable game scripts"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Anonymous network pools. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Anonymous network pools. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nAdd per-source connection/stream/rate quotas, close origins after repeated timeouts, reserve capacity across origins, place unrar behind a small global and per-origin semaphore, and enforce absolute transfer deadlines.",
+ "text": "Resource exhaustion: Anonymous network pools. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nAdd per-source connection/stream/rate quotas, close origins after repeated timeouts, reserve capacity across origins, place unrar behind a small global and per-origin semaphore, and enforce absolute transfer deadlines."
+ },
+ "id": "resource-exhaustion.anonymous-network-pools",
+ "name": "Resource exhaustion: Anonymous network pools",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Anonymous network pools"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Call to play rendering. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Call to play rendering. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nEnforce small per-author/global active-call caps before publication, render only a ranked ticker subset, virtualize/paginate the overlay, and avoid recomputing unchanged projections on every clock tick.",
+ "text": "Resource exhaustion: Call to play rendering. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nEnforce small per-author/global active-call caps before publication, render only a ranked ticker subset, virtualize/paginate the overlay, and avoid recomputing unchanged projections on every clock tick."
+ },
+ "id": "resource-exhaustion.call-to-play-rendering",
+ "name": "Resource exhaustion: Call to play rendering",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Call to play rendering"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Download retry total budget. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Download retry total budget. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nAdd total per-chunk/download wall-clock and attempt budgets independent of source count, cap automatically tried identities, add per-origin identity quotas, and require explicit user retry after budget exhaustion.",
+ "text": "Resource exhaustion: Download retry total budget. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nAdd total per-chunk/download wall-clock and attempt budgets independent of source count, cap automatically tried identities, add per-origin identity quotas, and require explicit user retry after budget exhaustion."
+ },
+ "id": "resource-exhaustion.download-retry-total-budget",
+ "name": "Resource exhaustion: Download retry total budget",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Download retry total budget"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Length prefix eager reserve. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Length prefix eager reserve. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nUse a much smaller inbound Request-frame maximum; gate declared response bytes through a global memory semaphore or incremental buffering; reduce discovery fan-out as defense in depth.",
+ "text": "Resource exhaustion: Length prefix eager reserve. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nUse a much smaller inbound Request-frame maximum; gate declared response bytes through a global memory semaphore or incremental buffering; reduce discovery fan-out as defense in depth."
+ },
+ "id": "resource-exhaustion.length-prefix-eager-reserve",
+ "name": "Resource exhaustion: Length prefix eager reserve",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Length prefix eager reserve"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Local library ingestion. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Local library ingestion. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nIntersect names with the catalog before retention/task admission; add root/per-game/depth/metadata budgets and a bounded rescan pool; bounded-read current and legacy indexes/version.ini and avoid raw invalid-content logging.",
+ "text": "Resource exhaustion: Local library ingestion. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nIntersect names with the catalog before retention/task admission; add root/per-game/depth/metadata budgets and a bounded rescan pool; bounded-read current and legacy indexes/version.ini and avoid raw invalid-content logging."
+ },
+ "id": "resource-exhaustion.local-library-ingestion",
+ "name": "Resource exhaustion: Local library ingestion",
+ "properties": {
+ "security-severity": "2.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Local library ingestion"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Manifest cache admission order. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Manifest cache admission order. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nCheck `content_identity` and local readiness first, then require `cached_manifest`; add tests proving rejected requests perform no load, plus an aggregate cache budget.",
+ "text": "Resource exhaustion: Manifest cache admission order. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nCheck `content_identity` and local readiness first, then require `cached_manifest`; add tests proving rejected requests perform no load, plus an aggregate cache budget."
+ },
+ "id": "resource-exhaustion.manifest-cache-admission-order",
+ "name": "Resource exhaustion: Manifest cache admission order",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Manifest cache admission order"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Mdns candidate monopoly. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Mdns candidate monopoly. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nAdd per-source-IP quotas, preserve capacity across origins/known peers, and use fair/randomized replacement or a cheap proof-of-key before the longer negotiation slot.",
+ "text": "Resource exhaustion: Mdns candidate monopoly. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nAdd per-source-IP quotas, preserve capacity across origins/known peers, and use fair/randomized replacement or a cheap proof-of-key before the longer negotiation slot."
+ },
+ "id": "resource-exhaustion.mdns-candidate-monopoly",
+ "name": "Resource exhaustion: Mdns candidate monopoly",
+ "properties": {
+ "security-severity": "2.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Mdns candidate monopoly"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Sybil aggregate state. Categories: Resource exhaustion. Weaknesses: CWE-400."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Sybil aggregate state. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\n## Remediation\n\nEnforce aggregate byte/event budgets across remote peers, coalesce complete UI state through bounded latest-value channels, avoid full cloning per commit, and add fair/per-origin admission for identities.",
+ "text": "Resource exhaustion: Sybil aggregate state. Categories: Resource exhaustion. Weaknesses: CWE-400.\n\nRemediation:\n\nEnforce aggregate byte/event budgets across remote peers, coalesce complete UI state through bounded latest-value channels, avoid full cloning per commit, and add fair/per-origin admission for identities."
+ },
+ "id": "resource-exhaustion.sybil-aggregate-state",
+ "name": "Resource exhaustion: Sybil aggregate state",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "external/cwe/cwe-400",
+ "resource-exhaustion",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Sybil aggregate state"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "Resource exhaustion: Unauthenticated state hint proxy. Categories: Confused deputy. Weaknesses: CWE-441."
+ },
+ "help": {
+ "markdown": "Resource exhaustion: Unauthenticated state hint proxy. Categories: Confused deputy. Weaknesses: CWE-441.\n\n## Remediation\n\nIf requester authentication remains absent, drop remote change hints and rely on pinned liveness reconciliation. Otherwise bind the hint to an authenticated client PeerId and remove the payload identity.",
+ "text": "Resource exhaustion: Unauthenticated state hint proxy. Categories: Confused deputy. Weaknesses: CWE-441.\n\nRemediation:\n\nIf requester authentication remains absent, drop remote change hints and rely on pinned liveness reconciliation. Otherwise bind the hint to an authenticated client PeerId and remove the payload identity."
+ },
+ "id": "resource-exhaustion.unauthenticated-state-hint-proxy",
+ "name": "Resource exhaustion: Unauthenticated state hint proxy",
+ "properties": {
+ "security-severity": "5.0",
+ "tags": [
+ "confused-deputy",
+ "external/cwe/cwe-441",
+ "security"
+ ]
+ },
+ "shortDescription": {
+ "text": "Resource exhaustion: Unauthenticated state hint proxy"
+ }
+ },
+ {
+ "fullDescription": {
+ "text": "State integrity: Call to play event ID collision. Categories: State integrity. Weaknesses: CWE-694."
+ },
+ "help": {
+ "markdown": "State integrity: Call to play event ID collision. Categories: State integrity. Weaknesses: CWE-694.\n\n## Remediation\n\nUse `(author_id, event.id)` as the key for deduplication, messages, React keys, and ordering; add cross-author collision tests including collision with a creator's Create event.",
+ "text": "State integrity: Call to play event ID collision. Categories: State integrity. Weaknesses: CWE-694.\n\nRemediation:\n\nUse `(author_id, event.id)` as the key for deduplication, messages, React keys, and ordering; add cross-author collision tests including collision with a creator's Create event."
+ },
+ "id": "state-integrity.call-to-play-event-id-collision",
+ "name": "State integrity: Call to play event ID collision",
+ "properties": {
+ "security-severity": "2.0",
+ "tags": [
+ "external/cwe/cwe-694",
+ "security",
+ "state-integrity"
+ ]
+ },
+ "shortDescription": {
+ "text": "State integrity: Call to play event ID collision"
+ }
+ }
+ ],
+ "version": "0.1.22"
+ }
+ }
+ }
+ ],
+ "version": "2.1.0"
+}
diff --git a/security-report/findings.json b/security-report/findings.json
new file mode 100644
index 0000000..022c318
--- /dev/null
+++ b/security-report/findings.json
@@ -0,0 +1,2685 @@
+{
+ "documentType": "codex-security.findings",
+ "findings": [
+ {
+ "attackPath": {
+ "controls": [
+ "64-peer and 64-author count caps",
+ "4 MiB per-author encoded cap",
+ "TLS responder pinning"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "sybil-author-cap",
+ "sybil-full-projection",
+ "sybil-unbounded-ui",
+ "sybil-wire-bounds"
+ ],
+ "outcome": "CPU/memory exhaustion and exclusion of legitimate new peers.",
+ "sink": "Desktop process heap and peer-to-Tauri event queue.",
+ "source": "Attacker-operated mDNS/QUIC responders with valid keys.",
+ "summary": "Self-issued identities -> pinned snapshots -> retained per-peer/author maps -> cloned aggregate projections -> unbounded Tauri event queue."
+ },
+ "impact": {
+ "level": "high",
+ "rationale": "The application may freeze, swap heavily, or terminate and legitimate peers may be excluded."
+ },
+ "likelihood": {
+ "level": "medium",
+ "rationale": "The attack is practical but requires many identities and substantial traffic."
+ },
+ "preconditions": [
+ "Sharing/discovery enabled",
+ "Dozens of distinct PeerIds and endpoints",
+ "Sustained valid snapshot traffic"
+ ],
+ "reachability": {
+ "attacker": "Malicious same-LAN host.",
+ "entrypoint": "mDNS candidate negotiation and Hello snapshot commit.",
+ "evidenceRefs": [
+ "sybil-author-cap",
+ "sybil-full-projection",
+ "sybil-unbounded-ui",
+ "sybil-wire-bounds"
+ ],
+ "preconditions": [
+ "Sharing/discovery enabled",
+ "Dozens of distinct PeerIds and endpoints",
+ "Sustained valid snapshot traffic"
+ ],
+ "summary": "Reachable from the same LAN while sharing/discovery is enabled; no victim key compromise is required."
+ },
+ "summary": "Attacker-created identities authenticate as themselves, fill retained slots, publish maximal valid state, and trigger full queued projections."
+ },
+ "codeEvidence": [
+ {
+ "code": "if self.remote.len() >= MAX_CALL_TO_PLAY_AUTHORS - 1 {\n return ObserveRemoteAuthorOutcome::AtCapacity;\n}\nself.remote.insert(author_id, RemoteAuthorSlice { ... });",
+ "endLine": 565,
+ "explanation": "The cap counts identities, not aggregate encoded bytes or physical origins.",
+ "id": "sybil-author-cap",
+ "label": "Per-author state is retained until a global author-count cap",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/call_to_play.rs",
+ "role": "root_control",
+ "startLine": 555
+ },
+ {
+ "code": "for (author_id, slice) in &self.remote {\n Self::extend_visible_author_events(*author_id, &slice.snapshot, now, &windows, &mut events);\n}\nevents.sort_by_key(|event| (event.at, event.call_id, event.author_id, event.id));",
+ "endLine": 855,
+ "explanation": "Repeated commits rebuild complete owned views.",
+ "id": "sybil-full-projection",
+ "label": "Every publication clones visible events across all authors",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/call_to_play.rs",
+ "role": "evidence",
+ "startLine": 835
+ },
+ {
+ "code": "pub fn send(tx_notify_ui: &UnboundedSender, event: PeerEvent) {\n if let Err(err) = tx_notify_ui.send(event) { ... }\n}",
+ "endLine": 25,
+ "explanation": "There is no capacity, backpressure, or replace-latest coalescing at this boundary.",
+ "id": "sybil-unbounded-ui",
+ "label": "Complete views enter an unbounded channel",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/events.rs",
+ "role": "sink",
+ "startLine": 21
+ },
+ {
+ "code": "pub const MAX_LIBRARY_GAMES: usize = 4_096;\npub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;\npub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;",
+ "endLine": 20,
+ "explanation": "Sixty-three remote authors can retain about 252 MiB encoded before allocation overhead.",
+ "id": "sybil-wire-bounds",
+ "label": "Large limits are per author/peer",
+ "language": "rust",
+ "path": "crates/lanspread-proto/src/lib.rs",
+ "role": "source",
+ "startLine": 15
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "The identity generation, first-come caps, per-author byte allowance, full projection cloning, and unbounded sender are explicit in source."
+ },
+ "extensions": {},
+ "findingId": "csf_7f1f254725189aac18ef1e44",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:98060f19cf12dd314a2039a9dc1eb93158ac43200f525155125360ac5f7b7a9c"
+ },
+ "identity": {
+ "anchor": "sybil-aggregate-state-and-ui-publication"
+ },
+ "locations": [
+ {
+ "endLine": 565,
+ "path": "crates/lanspread-peer/src/call_to_play.rs",
+ "role": "root_control",
+ "startLine": 555
+ },
+ {
+ "endLine": 25,
+ "path": "crates/lanspread-peer/src/events.rs",
+ "role": "sink",
+ "startLine": 21
+ },
+ {
+ "endLine": 360,
+ "path": "crates/lanspread-peer/src/peer_db.rs",
+ "role": "root_control",
+ "startLine": 356
+ },
+ {
+ "endLine": 20,
+ "path": "crates/lanspread-proto/src/lib.rs",
+ "role": "source",
+ "startLine": 15
+ }
+ ],
+ "occurrenceId": "occ_b1ba111f96d0b687d00e5778",
+ "provenance": {
+ "candidateId": "cand-sybil-aggregate-state",
+ "originalCandidates": [
+ {
+ "confidence": "high",
+ "cwe": "CWE-400",
+ "locations": "identity.rs; discovery.rs; proto/lib.rs; peer_db.rs; call_to_play.rs; remote_state.rs; events.rs",
+ "severity": "high",
+ "source_to_sink": "Aggregate 64-peer/author state and cloned full views enter an unbounded UI queue.",
+ "title": "Sybil peer snapshots can exhaust memory through aggregate state retention and unbounded full-view publication"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Enforce aggregate byte/event budgets across remote peers, coalesce complete UI state through bounded latest-value channels, avoid full cloning per commit, and add fair/per-origin admission for identities.",
+ "rootCause": {
+ "evidenceRefs": [
+ "sybil-author-cap",
+ "sybil-full-projection",
+ "sybil-unbounded-ui",
+ "sybil-wire-bounds"
+ ],
+ "summary": "Identity-count limits are first-come and per-author bounds are not paired with aggregate retained-byte or queued-view budgets."
+ },
+ "ruleId": "resource-exhaustion.sybil-aggregate-state",
+ "severity": {
+ "changeConditions": "Severity increases if fewer identities can cross the host memory limit or if the UI consumer is routinely slow.",
+ "level": "medium",
+ "rationale": "The impact can terminate the desktop process, but exploitation requires sustained LAN access, dozens of identities/endpoints, and substantial state transfer."
+ },
+ "summary": "One LAN host can generate enough valid peer identities to fill peer/author slots, retain hundreds of MiB of bounded-per-author state, and repeatedly enqueue cloned full views without an aggregate byte budget.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "Sybil peers can exhaust aggregate state and unbounded UI publication",
+ "validation": {
+ "assertions": [
+ "One host can create many valid self-issued identities.",
+ "Per-author 4 MiB limits aggregate across 63 remote authors.",
+ "Full replacement views are owned clones sent through an unbounded channel."
+ ],
+ "counterEvidence": [
+ "Peer and author counts are finite.",
+ "TLS prevents impersonation of an existing PeerId.",
+ "Invalid snapshots and wrong content bytes are rejected."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "sybil-author-cap",
+ "sybil-full-projection",
+ "sybil-unbounded-ui",
+ "sybil-wire-bounds"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated and merged with the duplicate peer/author-slot candidate; exact content verification limits integrity impact but does not bound retained/queued state."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "Finite global caps",
+ "10-second control deadlines",
+ "10-minute inactivity deadlines",
+ "Separate control and bulk pools"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "pool-no-client-auth",
+ "pool-global-limits",
+ "pool-bulk-admission",
+ "pool-unrar-listing"
+ ],
+ "outcome": "Denial of legitimate sync/download/install service.",
+ "sink": "Global peer admission, bulk transfer capacity, native subprocess/CPU resources.",
+ "source": "Same-LAN client requests.",
+ "summary": "Anonymous QUIC connection -> global stream/bulk permit -> request handler -> file handles or unrar subprocesses."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "Availability is lost and host resources are pressured; file integrity remains catalog-protected."
+ },
+ "likelihood": {
+ "level": "high",
+ "rationale": "Only ordinary LAN reachability and sustained requests are required."
+ },
+ "preconditions": [
+ "Sharing enabled",
+ "Victim advertises or attacker learns endpoint",
+ "For StreamInstall, a locally available stream-capable game"
+ ],
+ "reachability": {
+ "attacker": "Any host on the reachable LAN.",
+ "entrypoint": "QUIC listener and StreamInstall/GetGameFileChunk requests.",
+ "evidenceRefs": [
+ "pool-no-client-auth",
+ "pool-global-limits",
+ "pool-bulk-admission",
+ "pool-unrar-listing"
+ ],
+ "preconditions": [
+ "Sharing enabled",
+ "Victim advertises or attacker learns endpoint",
+ "For StreamInstall, a locally available stream-capable game"
+ ],
+ "summary": "Directly reachable whenever Local network sharing is enabled."
+ },
+ "summary": "One LAN origin repeatedly opens connections/streams and valid bulk requests until every global slot is occupied."
+ },
+ "codeEvidence": [
+ {
+ "code": "let mut config = ServerConfig::builder_with_provider(provider)\n .with_protocol_versions(&[&rustls::version::TLS13])?\n .with_no_client_auth()\n .with_single_cert(...)?;",
+ "endLine": 133,
+ "explanation": "Requester identity cannot support fair per-peer quotas.",
+ "id": "pool-no-client-auth",
+ "label": "Server accepts anonymous requesters",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/tls.rs",
+ "role": "root_control",
+ "startLine": 126
+ },
+ {
+ "code": "const MAX_ESTABLISHED_CONNECTIONS: usize = 64;\nconst MAX_CONTROL_STREAM_TASKS: usize = 32;\nconst MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;\nconst MAX_GLOBAL_BULK_TRANSFER_TASKS: usize = 48;",
+ "endLine": 44,
+ "explanation": "One origin can consume every shared slot.",
+ "id": "pool-global-limits",
+ "label": "Admission budgets are global",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/server.rs",
+ "role": "root_control",
+ "startLine": 31
+ },
+ {
+ "code": "let bulk_permit = Arc::clone(&bulk_transfer_permits).try_acquire_owned();\ndrop(control_permit.take());\nif let Ok(permit) = bulk_permit {\n _bulk_permit = Some(permit);\n ...\n}",
+ "endLine": 89,
+ "explanation": "There is no source-address, requester, game, or expensive-provider quota.",
+ "id": "pool-bulk-admission",
+ "label": "Each bulk request independently takes the shared pool",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/stream.rs",
+ "role": "entrypoint",
+ "startLine": 70
+ },
+ {
+ "code": "let process = ScopedProcess::spawn(\n program,\n [\"vt\", \"-c-\", \"-p-\", ...],\n cancel_token,\n LISTING_CAPTURE_LIMIT,\n)?;",
+ "endLine": 414,
+ "explanation": "Each admitted operation can start native listing and later decompression.",
+ "id": "pool-unrar-listing",
+ "label": "Stream Install starts native archive processing",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/stream_install.rs",
+ "role": "sink",
+ "startLine": 387
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "No-client-auth, global-only semaphores, request dispatch, and per-request provider execution are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_984da905a9b4074194d1814b",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:1d70a00f495ad3463b91c2fcf08d4d7fcd7d3ff00cf1fc63b4a96b46ba0008b7"
+ },
+ "identity": {
+ "anchor": "anonymous-global-pools-and-stream-install-workers"
+ },
+ "locations": [
+ {
+ "endLine": 133,
+ "path": "crates/lanspread-peer/src/tls.rs",
+ "role": "root_control",
+ "startLine": 126
+ },
+ {
+ "endLine": 44,
+ "path": "crates/lanspread-peer/src/services/server.rs",
+ "role": "root_control",
+ "startLine": 31
+ },
+ {
+ "endLine": 89,
+ "path": "crates/lanspread-peer/src/services/stream.rs",
+ "role": "entrypoint",
+ "startLine": 70
+ },
+ {
+ "endLine": 414,
+ "path": "crates/lanspread-peer/src/stream_install.rs",
+ "role": "sink",
+ "startLine": 387
+ }
+ ],
+ "occurrenceId": "occ_4b80e3b7dfe1e6768d05ba94",
+ "provenance": {
+ "candidateId": "cand-anonymous-global-pool-dos",
+ "originalCandidates": [
+ {
+ "title": "Anonymous global pools"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Add per-source connection/stream/rate quotas, close origins after repeated timeouts, reserve capacity across origins, place unrar behind a small global and per-origin semaphore, and enforce absolute transfer deadlines.",
+ "rootCause": {
+ "evidenceRefs": [
+ "pool-no-client-auth",
+ "pool-global-limits",
+ "pool-bulk-admission",
+ "pool-unrar-listing"
+ ],
+ "summary": "Anonymous requester admission is protected only by finite global pools; expensive StreamInstall provider work has no smaller global or per-origin quota."
+ },
+ "ruleId": "resource-exhaustion.anonymous-network-pools",
+ "severity": {
+ "changeConditions": "Severity increases on memory-constrained hosts or archives with expensive decompression.",
+ "level": "medium",
+ "rationale": "A same-LAN attacker can reliably deny synchronization and transfers and impose expensive native work, but all pools are finite and the attack must be sustained."
+ },
+ "summary": "The responder authenticates no client and applies only global connection, control, and bulk limits, so one origin can occupy every admission slot and drive up to 48 concurrent StreamInstall operations with native unrar work.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "Anonymous LAN requesters can monopolize global pools and native extractors",
+ "validation": {
+ "assertions": [
+ "No client identity exists.",
+ "All resource permits are shared globally.",
+ "StreamInstall performs native work after acquiring only the general bulk permit."
+ ],
+ "counterEvidence": [
+ "Handshake, connection, control, and bulk counts are finite.",
+ "Control and bulk pools are separated.",
+ "Stalled work has application timeouts and cancellation."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "pool-no-client-auth",
+ "pool-global-limits",
+ "pool-bulk-admission",
+ "pool-unrar-listing"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated; the separate StreamInstall-subprocess candidate is merged here because it shares anonymous global bulk admission and the same quota remediation."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "128 MiB individual manifest limit",
+ "Finite immutable catalog",
+ "Serialized operation admission"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "cache-load-before-check",
+ "cache-stream-before-check",
+ "cache-unbounded-insert"
+ ],
+ "outcome": "Memory growth and denial/delay of legitimate operations.",
+ "sink": "Process heap, manifest parser, and serialized operation-admission lock.",
+ "source": "Unauthenticated bulk request fields.",
+ "summary": "Network game_id -> CatalogBundle::manifest -> bounded disk read/parse -> persistent cache -> later rejection."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "Potential process memory exhaustion and operational blocking; exact magnitude depends on external corpus."
+ },
+ "likelihood": {
+ "level": "high",
+ "rationale": "Wrong ContentIds are sufficient and no local availability is required."
+ },
+ "preconditions": [
+ "Sharing enabled",
+ "Knowledge or enumeration of bundled game IDs"
+ ],
+ "reachability": {
+ "attacker": "Same-LAN anonymous requester.",
+ "entrypoint": "GetGameFileChunk and StreamInstall.",
+ "evidenceRefs": [
+ "cache-load-before-check",
+ "cache-stream-before-check",
+ "cache-unbounded-insert"
+ ],
+ "preconditions": [
+ "Sharing enabled",
+ "Knowledge or enumeration of bundled game IDs"
+ ],
+ "summary": "Reachable for any known bundled game ID while sharing is enabled."
+ },
+ "summary": "An anonymous requester enumerates known catalog IDs with wrong ContentIds, forcing rejected requests to load/cache bodies."
+ },
+ "codeEvidence": [
+ {
+ "code": "let manifest = load_expected_catalog_manifest(ctx, game_id)?;\nif manifest.content_id() != content_id { return None; }\nif !can_serve_game(ctx, &game_dir, game_id).await { return None; }",
+ "endLine": 284,
+ "explanation": "A wrong ContentId or nonlocal known game still causes a body load.",
+ "id": "cache-load-before-check",
+ "label": "Full body loads precede rejection",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/transfer.rs",
+ "role": "root_control",
+ "startLine": 270
+ },
+ {
+ "code": "let manifest = load_expected_catalog_manifest(ctx, game_id)?;\nif manifest.content_id() != content_id { return None; }\nif !can_serve_game(ctx, &game_dir, game_id).await || !manifest.supports_streamed_install() { return None; }",
+ "endLine": 319,
+ "explanation": "Both independent network operations are affected.",
+ "id": "cache-stream-before-check",
+ "label": "Stream Install repeats the ordering",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/transfer.rs",
+ "role": "evidence",
+ "startLine": 308
+ },
+ {
+ "code": "let manifest = Arc::new(self.load_uncached(game_id, expected_version)?);\nlet mut cache = self.cache.write()?;\nOk(cache.entry(game_id.to_owned()).or_insert_with(|| Arc::clone(&manifest)).clone())",
+ "endLine": 186,
+ "explanation": "No entry/byte eviction applies to cumulative remote-triggered loads.",
+ "id": "cache-unbounded-insert",
+ "label": "Every miss is retained",
+ "language": "rust",
+ "path": "crates/lanspread-db/src/content_manifest/store.rs",
+ "role": "sink",
+ "startLine": 162
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "Call order and unbounded cache insertion are direct; the missing production bodies only limit measurement."
+ },
+ "extensions": {},
+ "findingId": "csf_28dcda78d4a8e1d179a20e6f",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:c71b31921c2ee1ae38de77962c2bfd27469df63611c19a80e083c349ee24daca"
+ },
+ "identity": {
+ "anchor": "full-manifest-load-before-compact-admission"
+ },
+ "locations": [
+ {
+ "endLine": 321,
+ "path": "crates/lanspread-peer/src/services/transfer.rs",
+ "role": "root_control",
+ "startLine": 258
+ },
+ {
+ "endLine": 186,
+ "path": "crates/lanspread-db/src/content_manifest/store.rs",
+ "role": "sink",
+ "startLine": 162
+ }
+ ],
+ "occurrenceId": "occ_4c4e343d108a709a37ed2015",
+ "provenance": {
+ "candidateId": "cand-manifest-cache-before-admission",
+ "originalCandidates": [
+ {
+ "title": "Manifest cache admission ordering"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Check `content_identity` and local readiness first, then require `cached_manifest`; add tests proving rejected requests perform no load, plus an aggregate cache budget.",
+ "rootCause": {
+ "evidenceRefs": [
+ "cache-load-before-check",
+ "cache-stream-before-check",
+ "cache-unbounded-insert"
+ ],
+ "summary": "Bulk admission uses the disk-capable on-demand manifest API before the compact index and preloaded local-library cache can reject the request."
+ },
+ "ruleId": "resource-exhaustion.manifest-cache-admission-order",
+ "severity": {
+ "changeConditions": "Severity depends on the aggregate encoded/parsed size of the external production manifest corpus.",
+ "level": "medium",
+ "rationale": "An anonymous LAN client can force persistent corpus-wide parse/retention and serialized admission work; actual production size is unavailable, limiting impact certainty."
+ },
+ "summary": "Both bulk request variants load and retain a full catalog manifest before checking compact ContentId equality or whether the game is locally serveable.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "Rejected bulk requests can populate the persistent manifest cache",
+ "validation": {
+ "assertions": [
+ "The compact identity is available without filesystem I/O.",
+ "Both bulk variants call full load before identity/local checks.",
+ "Loaded bodies persist in a HashMap without eviction."
+ ],
+ "counterEvidence": [
+ "Only immutable catalog IDs can load.",
+ "Each manifest and catalog are finite.",
+ "Operation admission serializes loads.",
+ "Production bodies are absent, so actual aggregate size was not measured."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "cache-load-before-check",
+ "cache-stream-before-check",
+ "cache-unbounded-insert"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated. Known IDs and per-body limits bound the universe, but do not enforce the documented no-I/O rejection or an aggregate retained-byte limit."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "64-decoder/candidate caps",
+ "10-second application deadlines",
+ "QUIC transport limits"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "prefix-frame-max",
+ "prefix-decoder-count",
+ "prefix-decoder-config",
+ "prefix-lock"
+ ],
+ "outcome": "Abrupt memory pressure, swapping, or process termination.",
+ "sink": "Desktop heap and every global control/discovery slot.",
+ "source": "Unauthenticated inbound clients or attacker-operated discovery responders.",
+ "summary": "Length prefix -> LengthDelimitedCodec header decode -> eager BytesMut reserve -> incomplete frame wait."
+ },
+ "impact": {
+ "level": "high",
+ "rationale": "A half-gigabyte allocation spike can terminate constrained desktops."
+ },
+ "likelihood": {
+ "level": "medium",
+ "rationale": "Inbound requires only two connections; discovery requires more setup. Waves are finite but repeatable."
+ },
+ "preconditions": [
+ "Sharing/discovery enabled",
+ "Concurrent streams or candidates"
+ ],
+ "reachability": {
+ "attacker": "Same-LAN participant.",
+ "entrypoint": "Server bidirectional streams and discovery Hello responses.",
+ "evidenceRefs": [
+ "prefix-frame-max",
+ "prefix-decoder-count",
+ "prefix-decoder-config",
+ "prefix-lock"
+ ],
+ "preconditions": [
+ "Sharing/discovery enabled",
+ "Concurrent streams or candidates"
+ ],
+ "summary": "Inbound requires listener access; discovery requires 64 advertised identities/endpoints with matching keys."
+ },
+ "summary": "Concurrent requesters or attacker-owned responders send maximal length prefixes and stall before payload."
+ },
+ "codeEvidence": [
+ {
+ "code": "pub const MAX_CONTROL_FRAME_BYTES: usize = 8 * 1024 * 1024;\npub const MAX_STREAM_INSTALL_FRAME_BYTES: usize = 8 * 1024 * 1024;",
+ "endLine": 14,
+ "explanation": "Inbound requests are much smaller, but share the response-sized maximum.",
+ "id": "prefix-frame-max",
+ "label": "Every control codec permits 8 MiB",
+ "language": "rust",
+ "path": "crates/lanspread-proto/src/lib.rs",
+ "role": "root_control",
+ "startLine": 13
+ },
+ {
+ "code": "const MAX_CONTROL_STREAM_TASKS: usize = 32;\nconst MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;",
+ "endLine": 41,
+ "explanation": "Two connections can populate all 64 stream tasks.",
+ "id": "prefix-decoder-count",
+ "label": "Server admits 64 global decoders",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/server.rs",
+ "role": "evidence",
+ "startLine": 31
+ },
+ {
+ "code": "LengthDelimitedCodec::builder()\n .max_frame_length(MAX_CONTROL_FRAME_BYTES)\n .new_codec()\n...\nlet first_frame = read_expected_frame(...).await;",
+ "endLine": 67,
+ "explanation": "The whole declared body is awaited before Request::decode.",
+ "id": "prefix-decoder-config",
+ "label": "Decoder uses the shared maximum before parsing",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/stream.rs",
+ "role": "entrypoint",
+ "startLine": 37
+ },
+ {
+ "code": "[[package]]\nname = \"tokio-util\"\nversion = \"0.7.19\"\nchecksum = \"494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52\"",
+ "endLine": 4659,
+ "explanation": "Offline inspection of this locked source confirmed `src.reserve(n.saturating_sub(src.len()))` after decoding the header.",
+ "id": "prefix-lock",
+ "label": "Locked decoder version",
+ "language": "text",
+ "path": "Cargo.lock",
+ "role": "evidence",
+ "startLine": 4655
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "Repository concurrency/frame limits and the locked dependency's reserve behavior were inspected offline."
+ },
+ "extensions": {},
+ "findingId": "csf_d40a863e76a3bf4cb0bd412a",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:fffdefc91a869c1291f679a54297d18c889dc2e049b391e4534d0a53e61769cf"
+ },
+ "identity": {
+ "anchor": "max-frame-prefix-reserves-aggregate-memory"
+ },
+ "locations": [
+ {
+ "endLine": 20,
+ "path": "crates/lanspread-proto/src/lib.rs",
+ "role": "root_control",
+ "startLine": 13
+ },
+ {
+ "endLine": 41,
+ "path": "crates/lanspread-peer/src/services/server.rs",
+ "role": "root_control",
+ "startLine": 31
+ },
+ {
+ "endLine": 67,
+ "path": "crates/lanspread-peer/src/services/stream.rs",
+ "role": "entrypoint",
+ "startLine": 37
+ },
+ {
+ "endLine": 32,
+ "path": "crates/lanspread-peer/src/services/discovery.rs",
+ "role": "entrypoint",
+ "startLine": 30
+ },
+ {
+ "endLine": 4659,
+ "path": "Cargo.lock",
+ "role": "evidence",
+ "startLine": 4655
+ }
+ ],
+ "occurrenceId": "occ_86bfac07819428f756982d0e",
+ "provenance": {
+ "candidateId": "cand-inbound-prefix-allocation",
+ "originalCandidates": [
+ {
+ "title": "Inbound prefix allocation"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Use a much smaller inbound Request-frame maximum; gate declared response bytes through a global memory semaphore or incremental buffering; reduce discovery fan-out as defense in depth.",
+ "rootCause": {
+ "evidenceRefs": [
+ "prefix-frame-max",
+ "prefix-decoder-count",
+ "prefix-decoder-config",
+ "prefix-lock"
+ ],
+ "summary": "A response-sized per-frame maximum is applied independently to many untrusted decoders without an aggregate receive-memory budget or incremental allocation."
+ },
+ "ruleId": "resource-exhaustion.length-prefix-eager-reserve",
+ "severity": {
+ "changeConditions": "Severity increases on memory-constrained desktops or if concurrency/frame limits grow.",
+ "level": "medium",
+ "rationale": "A LAN attacker can induce abrupt half-gigabyte allocation pressure with negligible payload, but global caps and 10-second deadlines bound each wave."
+ },
+ "summary": "The same 8 MiB control-frame allowance is multiplied across 64 concurrent inbound request decoders and 64 discovery Hello decoders; locked tokio-util eagerly reserves the declared body after only the length prefix.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "Control-frame prefixes can reserve about 512 MiB across concurrent decoders",
+ "validation": {
+ "assertions": [
+ "64 multiplied by 8 MiB is 512 MiB before overhead.",
+ "Only four prefix bytes are needed before tokio-util reserves.",
+ "Inbound and discovery response paths both use the shared maximum."
+ ],
+ "counterEvidence": [
+ "Global task/candidate caps bound a single wave.",
+ "Ten-second deadlines release stalled decoders.",
+ "Discovery identities must authenticate as themselves, though inbound clients are anonymous."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "prefix-frame-max",
+ "prefix-decoder-count",
+ "prefix-decoder-config",
+ "prefix-lock"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated and merged with the discovery-prefix instance because both use the same locked decoder behavior and aggregate-budget failure."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "Single-component game IDs",
+ "Catalog integrity for peer downloads",
+ "UAC prompt",
+ "Installed/local presence checks"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "runas-game-path",
+ "runas-server-path",
+ "runas-verb"
+ ],
+ "outcome": "Administrator execution of attacker-controlled batch content.",
+ "sink": "Elevated Windows command processor.",
+ "source": "Locally/shared-tree controlled game scripts.",
+ "summary": "Selected directory + game ID -> joined mutable script path -> string cmd.exe parameters -> ShellExecute runas."
+ },
+ "impact": {
+ "level": "high",
+ "rationale": "Successful exploitation executes attacker code with the elevated token."
+ },
+ "likelihood": {
+ "level": "medium",
+ "rationale": "Multiple local/user-interaction prerequisites materially constrain exploitation."
+ },
+ "preconditions": [
+ "Windows",
+ "Writable or crafted selected game root",
+ "Victim launch action",
+ "UAC approval"
+ ],
+ "reachability": {
+ "attacker": "Lower-privileged local or shared-directory supplier.",
+ "entrypoint": "run_game or start_server Tauri command.",
+ "evidenceRefs": [
+ "runas-game-path",
+ "runas-server-path",
+ "runas-verb"
+ ],
+ "preconditions": [
+ "Windows",
+ "Writable or crafted selected game root",
+ "Victim launch action",
+ "UAC approval"
+ ],
+ "summary": "Conditional on Windows, selected-root write influence, local launch, and UAC approval."
+ },
+ "summary": "A writable selected game tree supplies or replaces expected script names; a victim launch crosses runas."
+ },
+ "codeEvidence": [
+ {
+ "code": "let game_path = games_folder.join(id.clone());\nlet game_setup_bin = game_path.join(GAME_SETUP_SCRIPT);\nlet game_start_bin = game_path.join(GAME_START_SCRIPT);\n...\nrun_as_admin_and_wait(\"cmd.exe\", &setup_params, &game_dir, ...)?;\n...\nrun_as_admin_detached(\"cmd.exe\", &script_params(&game_start_bin, ...), ...);",
+ "endLine": 1951,
+ "explanation": "No manifest, digest, retained handle, or reparse check binds the launched script.",
+ "id": "runas-game-path",
+ "label": "Game scripts are path-checked then elevated",
+ "language": "rust",
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "sink",
+ "startLine": 1891
+ },
+ {
+ "code": "let game_path = games_folder.join(id.clone());\nlet server_start_bin = game_path.join(SERVER_START_SCRIPT);\nif !server_start_bin.is_file() { return Ok(false); }\n...\nrun_as_admin_detached(\"cmd.exe\", &server_script_params(&server_start_bin, ...), ...);",
+ "endLine": 2057,
+ "explanation": "Path-based `is_file` follows redirections and does not prove catalog authority.",
+ "id": "runas-server-path",
+ "label": "Server script has the same gap",
+ "language": "rust",
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "evidence",
+ "startLine": 2026
+ },
+ {
+ "code": "let runas_wide = OsStr::new(\"runas\").encode_wide().chain(Some(0)).collect();\nShellExecuteW(None, PCWSTR::from_raw(runas_wide.as_ptr()), ...);",
+ "endLine": 1577,
+ "explanation": "The explicit UAC boundary makes mutable-script provenance security-sensitive.",
+ "id": "runas-verb",
+ "label": "Detached launcher requests elevation",
+ "language": "rust",
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "root_control",
+ "startLine": 1563
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "The path checks, missing catalog/digest checks, and runas calls are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_440f1250535a69bcbbeb8d14",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:2d051e05bbf21a3a948ad8f9028519a29bd04980ea7c981575c9c956de7a0e20"
+ },
+ "identity": {
+ "anchor": "mutable-game-script-runas-without-integrity-binding"
+ },
+ "locations": [
+ {
+ "endLine": 1958,
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "sink",
+ "startLine": 1866
+ },
+ {
+ "endLine": 2064,
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "sink",
+ "startLine": 2007
+ },
+ {
+ "endLine": 1582,
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "root_control",
+ "startLine": 1554
+ }
+ ],
+ "occurrenceId": "occ_e3fcb8021db0f8fa3a50cdad",
+ "provenance": {
+ "candidateId": "cand-elevated-mutable-game-scripts",
+ "originalCandidates": [
+ {
+ "title": "Elevated mutable game scripts"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Run ordinary game/server scripts as the current user. For setup requiring elevation, require a catalog game and authoritative installed state, verify the exact script digest immediately before launch through no-follow handles, reject reparse points, and preserve the verified object identity into process creation.",
+ "rootCause": {
+ "evidenceRefs": [
+ "runas-game-path",
+ "runas-server-path",
+ "runas-verb"
+ ],
+ "summary": "The Windows launch path is separate from the catalog-bound no-follow install/download capabilities and treats filename/existence plus user action as sufficient authority for elevation."
+ },
+ "ruleId": "privilege-escalation.mutable-game-scripts",
+ "severity": {
+ "changeConditions": "Severity increases where selected game directories are shared across trust boundaries or UAC prompts are routinely approved.",
+ "level": "medium",
+ "rationale": "The consequence is administrator code execution, but exploitation requires local/shared-tree write influence, a victim launch action, Windows, and UAC approval."
+ },
+ "summary": "Windows play/server commands follow mutable paths under any selected game root and pass setup/start scripts to `cmd.exe` with `runas` without rechecking catalog membership, digest, installed ownership, or reparse-free containment.",
+ "taxonomy": {
+ "category": "privilege-escalation",
+ "cwe": [
+ "CWE-250",
+ "CWE-73"
+ ]
+ },
+ "title": "Mutable game scripts are launched elevated without launch-time trust binding",
+ "validation": {
+ "assertions": [
+ "Any existing selected directory can become the games root.",
+ "Sink functions accept single-component IDs without a catalog lookup.",
+ "Scripts are elevated by path after only existence/local checks."
+ ],
+ "counterEvidence": [
+ "IDs are one component and user settings are sanitized.",
+ "Downloaded/streamed bytes are catalog verified.",
+ "The victim must invoke play/server and approve UAC."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "runas-game-path",
+ "runas-server-path",
+ "runas-verb"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated with severity reduced from high for Windows/UAC/local-write prerequisites. Remote peers alone cannot substitute bytes because downloads remain catalog-verified."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "Per-author duplicate rejection",
+ "Creator-only action checks",
+ "Authenticated author attribution"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "ctp-per-author-id",
+ "ctp-author-preserved",
+ "ctp-global-map"
+ ],
+ "outcome": "Legitimate calls or updates disappear or are substituted.",
+ "sink": "Call-to-Play reducer and rendered nominations/messages.",
+ "source": "Attacker-controlled CallToPlayAuthorEvent.id.",
+ "summary": "Pinned remote snapshot -> authenticated Rust view with author_id -> frontend Map keyed only by event.id -> overwritten legitimate event."
+ },
+ "impact": {
+ "level": "low",
+ "rationale": "Only ephemeral collaboration UI integrity is affected."
+ },
+ "likelihood": {
+ "level": "high",
+ "rationale": "The nonce is visible to connected peers and can be copied exactly."
+ },
+ "preconditions": [
+ "Attacker observes target nonce",
+ "Attacker publishes a colliding valid event"
+ ],
+ "reachability": {
+ "attacker": "Authenticated hostile LAN peer.",
+ "entrypoint": "HelloSnapshot Call-to-Play author slice.",
+ "evidenceRefs": [
+ "ctp-per-author-id",
+ "ctp-author-preserved",
+ "ctp-global-map"
+ ],
+ "preconditions": [
+ "Attacker observes target nonce",
+ "Attacker publishes a colliding valid event"
+ ],
+ "summary": "Requires a connected hostile peer that observes the shared snapshot and publishes a later valid author slice."
+ },
+ "summary": "A hostile author republishes a valid participant event using a victim event's nonce so frontend deduplication keeps the hostile later event."
+ },
+ "codeEvidence": [
+ {
+ "code": "let mut event_ids = HashSet::with_capacity(snapshot.events.len());\n...\nif !event_ids.insert(event.id) {\n return Err(CallToPlayValidationError::DuplicateEventId(event.id));\n}",
+ "endLine": 979,
+ "explanation": "Equal nonces across authenticated authors remain valid.",
+ "id": "ctp-per-author-id",
+ "label": "Nonce uniqueness is per author snapshot",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/call_to_play.rs",
+ "role": "evidence",
+ "startLine": 961
+ },
+ {
+ "code": "output.push(CallToPlayViewEvent {\n id: event.id,\n call_id: event.call_id,\n author_id,\n author_name: snapshot.display_name.clone(),\n ...\n});",
+ "endLine": 884,
+ "explanation": "The correct composite identity is available to consumers.",
+ "id": "ctp-author-preserved",
+ "label": "Rust projection preserves the namespace",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/call_to_play.rs",
+ "role": "evidence",
+ "startLine": 877
+ },
+ {
+ "code": "const unique = new Map(input.map(event => [event.id, event]));\nconst byCall = new Map();\nfor (const event of unique.values()) { ... }",
+ "endLine": 145,
+ "explanation": "A later event with the same nonce replaces another author's event before grouping.",
+ "id": "ctp-global-map",
+ "label": "Frontend drops the author namespace",
+ "language": "typescript",
+ "path": "crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts",
+ "role": "sink",
+ "startLine": 135
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "Producer preserves author_id while the reducer's global Map key omits it."
+ },
+ "extensions": {},
+ "findingId": "csf_40c94234ad7f1c26e592c9b8",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:453e4b66813024523868694a6ea51b2d7948c85ceda20110a895465e3d26f7ae"
+ },
+ "identity": {
+ "anchor": "frontend-deduplicates-author-scoped-event-id-globally"
+ },
+ "locations": [
+ {
+ "endLine": 979,
+ "path": "crates/lanspread-peer/src/call_to_play.rs",
+ "role": "root_control",
+ "startLine": 961
+ },
+ {
+ "endLine": 145,
+ "path": "crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts",
+ "role": "sink",
+ "startLine": 135
+ }
+ ],
+ "occurrenceId": "occ_e884be7f0807f9071e8f54ec",
+ "provenance": {
+ "candidateId": "cand-cross-author-event-collision",
+ "originalCandidates": [
+ {
+ "title": "Cross-author event collision"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Use `(author_id, event.id)` as the key for deduplication, messages, React keys, and ordering; add cross-author collision tests including collision with a creator's Create event.",
+ "rootCause": {
+ "evidenceRefs": [
+ "ctp-per-author-id",
+ "ctp-author-preserved",
+ "ctp-global-map"
+ ],
+ "summary": "The frontend treats an author-scoped random nonce as a globally unique identifier."
+ },
+ "ruleId": "state-integrity.call-to-play-event-id-collision",
+ "severity": {
+ "changeConditions": "Impact grows if Call-to-Play state later triggers privileged actions automatically.",
+ "level": "low",
+ "rationale": "The attack can hide or substitute UI collaboration state but cannot forge authenticated author authority, modify files, or execute code."
+ },
+ "summary": "Rust validates EventNonce uniqueness only within each authenticated author's slice, but the frontend globally deduplicates events by nonce alone, letting a hostile author overwrite another author's event in the rendered view.",
+ "taxonomy": {
+ "category": "state-integrity",
+ "cwe": [
+ "CWE-694"
+ ]
+ },
+ "title": "Cross-author event-ID collisions can suppress Call-to-Play entries",
+ "validation": {
+ "assertions": [
+ "Hostile authors can observe shared event nonces.",
+ "Cross-author duplicates survive Rust validation.",
+ "Frontend Map overwrite occurs before call grouping."
+ ],
+ "counterEvidence": [
+ "Author identity is TLS-derived and preserved.",
+ "Creator-only actions cannot be forged.",
+ "Impact is confined to UI state."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "ctp-per-author-id",
+ "ctp-author-preserved",
+ "ctp-global-map"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated. Creator-only Rust authorization remains effective, so severity is low and limited to rendered-state integrity/availability."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "64-slot hint queue",
+ "8 concurrent pulls",
+ "5-second per-peer coalescing",
+ "Pinned TLS follow-up"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "hint-unbound-dispatch",
+ "hint-third-party-select",
+ "hint-full-pull"
+ ],
+ "outcome": "Bandwidth and CPU amplification/availability degradation.",
+ "sink": "Victim connector and unrelated peer's Hello responder.",
+ "source": "Unauthenticated ChangeHint fields.",
+ "summary": "Inbound hint -> claimed PeerId -> state-sync slot -> cached pinned endpoint -> full Hello exchange."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "Can consume significant bandwidth/serialization work but cannot inject authoritative state."
+ },
+ "likelihood": {
+ "level": "high",
+ "rationale": "No credentials or target private keys are required."
+ },
+ "preconditions": [
+ "Sharing enabled",
+ "Victim has known peers",
+ "Repeated forged session/revision values"
+ ],
+ "reachability": {
+ "attacker": "Any reachable LAN host.",
+ "entrypoint": "LibraryChanged or CallToPlayChanged.",
+ "evidenceRefs": [
+ "hint-unbound-dispatch",
+ "hint-third-party-select",
+ "hint-full-pull"
+ ],
+ "preconditions": [
+ "Sharing enabled",
+ "Victim has known peers",
+ "Repeated forged session/revision values"
+ ],
+ "summary": "Direct while sharing is enabled and at least one peer is known."
+ },
+ "summary": "An anonymous requester repeatedly claims stale sessions for known peers, turning the victim into a bounded proxy."
+ },
+ "codeEvidence": [
+ {
+ "code": "Request::LibraryChanged(hint) => {\n ctx.state_sync.schedule_hint(StateDomain::Library, hint);\n DispatchResult::close(framed_tx)\n}",
+ "endLine": 271,
+ "explanation": "The payload claim is accepted from any requester.",
+ "id": "hint-unbound-dispatch",
+ "label": "Inbound connection identity is not checked",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/stream.rs",
+ "role": "entrypoint",
+ "startLine": 265
+ },
+ {
+ "code": "let snapshot = ctx.peer_liveness_for(peer_id).await;\n...\nperform_peer_refresh(ctx, snapshot).await\n...\nif snapshot.runtime_session_id != trigger.hint.runtime_session_id { return true; }",
+ "endLine": 388,
+ "explanation": "A random session forces a pull of the named known peer.",
+ "id": "hint-third-party-select",
+ "label": "Claimed ID selects cached peer",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/state_sync.rs",
+ "role": "root_control",
+ "startLine": 351
+ },
+ {
+ "code": "let snapshot = exchange_hello(&ctx.quic, &endpoint, &ctx.cancellation).await?;\nlet prepared = PreparedSnapshot::prepare(endpoint.peer_id, generation, snapshot);\ncommit_prepared(ctx, endpoint, ticket, prepared).await",
+ "endLine": 159,
+ "explanation": "Authoritative state remains pinned, but resource work is delegated.",
+ "id": "hint-full-pull",
+ "label": "Reconciliation performs Hello",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/remote_state.rs",
+ "role": "sink",
+ "startLine": 147
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "The unbound payload identity, scheduler key, mismatch decision, and full-pull sink are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_f3f28e25fca367bd97e7cdac",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:c463c38e3d5499ce06d5279a12064947fd41f36deea0e7c6514b71d8077724a2"
+ },
+ "identity": {
+ "anchor": "unbound-hint-selects-third-party-pull"
+ },
+ "locations": [
+ {
+ "endLine": 271,
+ "path": "crates/lanspread-peer/src/services/stream.rs",
+ "role": "entrypoint",
+ "startLine": 265
+ },
+ {
+ "endLine": 388,
+ "path": "crates/lanspread-peer/src/services/state_sync.rs",
+ "role": "root_control",
+ "startLine": 351
+ },
+ {
+ "endLine": 159,
+ "path": "crates/lanspread-peer/src/services/remote_state.rs",
+ "role": "sink",
+ "startLine": 147
+ }
+ ],
+ "occurrenceId": "occ_dc1e0812adfa054e42cddf8e",
+ "provenance": {
+ "candidateId": "cand-unauthenticated-hint-proxy",
+ "originalCandidates": [
+ {
+ "title": "Unauthenticated hint proxy"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "If requester authentication remains absent, drop remote change hints and rely on pinned liveness reconciliation. Otherwise bind the hint to an authenticated client PeerId and remove the payload identity.",
+ "rootCause": {
+ "evidenceRefs": [
+ "hint-unbound-dispatch",
+ "hint-third-party-select",
+ "hint-full-pull"
+ ],
+ "summary": "A requester-anonymous protocol uses an untrusted claimed PeerId as a third-party work selector rather than binding or discarding hints."
+ },
+ "ruleId": "resource-exhaustion.unauthenticated-state-hint-proxy",
+ "severity": {
+ "changeConditions": "Severity increases with large valid snapshots or metered/slow links.",
+ "level": "medium",
+ "rationale": "Small anonymous requests can repeatedly consume victim and third-party bandwidth/serialization CPU, but concurrency, tracked peers, and per-peer cadence are bounded."
+ },
+ "summary": "Anonymous inbound connections may name any known PeerId in a change hint; a forged session/revision mismatch schedules a full pinned Hello pull to that unrelated peer.",
+ "taxonomy": {
+ "category": "confused-deputy",
+ "cwe": [
+ "CWE-441"
+ ]
+ },
+ "title": "Unauthenticated hints can make the victim pull arbitrary known peers",
+ "validation": {
+ "assertions": [
+ "Server has no client auth.",
+ "Claimed PeerId is the scheduler key.",
+ "Session mismatch forces a full pull."
+ ],
+ "counterEvidence": [
+ "Hint and pinned queues are bounded/separate.",
+ "At most eight pulls run concurrently.",
+ "Each peer is coalesced to one pull per five seconds.",
+ "Pulled state is TLS pinned."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "hint-unbound-dispatch",
+ "hint-third-party-select",
+ "hint-full-pull"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated. State integrity remains protected by the pinned pull, while resource consumption is the concrete impact."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "64 active/recent cap",
+ "5-second cooldown",
+ "Pinned TLS before commit"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "mdns-candidate-cap",
+ "mdns-preauth-admission",
+ "mdns-full-drop"
+ ],
+ "outcome": "Legitimate new peers are not discovered.",
+ "sink": "Discovery candidate capacity.",
+ "source": "Attacker-controlled multicast advertisements.",
+ "summary": "mDNS TXT/address -> PeerEndpoint -> active candidate set -> pinned negotiation -> failure/repeat."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "New peer discovery is denied; existing state and content remain protected."
+ },
+ "likelihood": {
+ "level": "high",
+ "rationale": "Only forged advertisements are required."
+ },
+ "preconditions": [
+ "Discovery enabled",
+ "Ability to advertise 64 unique records continuously"
+ ],
+ "reachability": {
+ "attacker": "Hostile LAN participant.",
+ "entrypoint": "mDNS browser service observations.",
+ "evidenceRefs": [
+ "mdns-candidate-cap",
+ "mdns-preauth-admission",
+ "mdns-full-drop"
+ ],
+ "preconditions": [
+ "Discovery enabled",
+ "Ability to advertise 64 unique records continuously"
+ ],
+ "summary": "Same multicast LAN while discovery is enabled."
+ },
+ "summary": "Forged unique mDNS records occupy all pre-auth negotiations and are replenished as they fail."
+ },
+ "codeEvidence": [
+ {
+ "code": "const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;\n...\nendpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr\n...\n|| self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES",
+ "endLine": 50,
+ "explanation": "Distinct IDs and ports from one host fill the entire budget.",
+ "id": "mdns-candidate-cap",
+ "label": "Attacker controls both uniqueness keys",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/discovery.rs",
+ "role": "root_control",
+ "startLine": 30
+ },
+ {
+ "code": "if let Some(endpoint) = validated_candidate_endpoint(&info) {\n ...\n let handshake = ReservedCandidateHandshake::reserve(handshake_ctx, endpoint).await?;\n active_candidates.insert(endpoint);\n negotiations.push(run_protocol_negotiation(...));\n}",
+ "endLine": 259,
+ "explanation": "TLS proof happens after the slot is occupied.",
+ "id": "mdns-preauth-admission",
+ "label": "Work starts from claimed fields",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/discovery.rs",
+ "role": "entrypoint",
+ "startLine": 233
+ },
+ {
+ "code": "active.len() < MAX_ACTIVE_DISCOVERY_CANDIDATES\n && !candidate_conflicts(active, candidate)\n && recent.try_record(candidate, now)",
+ "endLine": 309,
+ "explanation": "No fair queue or per-origin reserve remains for legitimate candidates.",
+ "id": "mdns-full-drop",
+ "label": "Full set drops every later candidate",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/discovery.rs",
+ "role": "sink",
+ "startLine": 300
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "Pre-auth admission, full-set rejection, and uniqueness keys are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_a8fd48f15b36d21c1eae743b",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:3354152dfaef2ebe80ded38bcc4d7469856a92511d005d07d97fb1fe3046376b"
+ },
+ "identity": {
+ "anchor": "preauth-mdns-candidates-fill-all-negotiation-slots"
+ },
+ "locations": [
+ {
+ "endLine": 50,
+ "path": "crates/lanspread-peer/src/services/discovery.rs",
+ "role": "root_control",
+ "startLine": 30
+ },
+ {
+ "endLine": 259,
+ "path": "crates/lanspread-peer/src/services/discovery.rs",
+ "role": "entrypoint",
+ "startLine": 233
+ },
+ {
+ "endLine": 309,
+ "path": "crates/lanspread-peer/src/services/discovery.rs",
+ "role": "sink",
+ "startLine": 294
+ }
+ ],
+ "occurrenceId": "occ_f3586624a8d633db89a319d4",
+ "provenance": {
+ "candidateId": "cand-mdns-discovery-monopoly",
+ "originalCandidates": [
+ {
+ "title": "mDNS candidate monopoly"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Add per-source-IP quotas, preserve capacity across origins/known peers, and use fair/randomized replacement or a cheap proof-of-key before the longer negotiation slot.",
+ "rootCause": {
+ "evidenceRefs": [
+ "mdns-candidate-cap",
+ "mdns-preauth-admission",
+ "mdns-full-drop"
+ ],
+ "summary": "First-come discovery capacity is keyed only by unauthenticated claimed PeerId and full socket address, with no origin fairness."
+ },
+ "ruleId": "resource-exhaustion.mdns-candidate-monopoly",
+ "severity": {
+ "changeConditions": "Severity rises if discovery is the only operational path and attacks persist throughout events.",
+ "level": "low",
+ "rationale": "The attack suppresses discovery of new peers but is same-LAN, requires sustained advertisements, and cannot commit state or affect already known authenticated peers."
+ },
+ "summary": "Sixty-four unique attacker-controlled PeerId/address hints occupy every active and recent candidate slot before TLS proof, causing legitimate new candidates to be dropped while the attacker rotates records.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "Forged mDNS candidates can monopolize discovery slots",
+ "validation": {
+ "assertions": [
+ "No private key is required to occupy a slot until handshake fails.",
+ "All later candidates are rejected at 64 active entries.",
+ "An attacker can rotate IDs/ports after deadlines."
+ ],
+ "counterEvidence": [
+ "Work is capped and timed.",
+ "Duplicate IDs/addresses coalesce.",
+ "No state commits without pinned TLS."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "mdns-candidate-cap",
+ "mdns-preauth-admission",
+ "mdns-full-drop"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated with low severity. The candidate cap protects memory but not fair availability."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "Link/reparse rejection",
+ "Special-file filtering",
+ "Per-ID rescan coalescing",
+ "Missed-tick skipping"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "local-all-root-entries",
+ "local-task-per-id",
+ "local-index-whole-read",
+ "legacy-whole-read"
+ ],
+ "outcome": "Persistent CPU/memory exhaustion or startup OOM.",
+ "sink": "Desktop heap, blocking pool, async task set, and log file.",
+ "source": "Locally or share-writable selected game directory and persisted index files.",
+ "summary": "Selected filesystem -> root snapshot/WalkDir/whole-file read -> task set/heap/logs."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "Can stall or terminate the peer but does not grant file escape or code execution."
+ },
+ "likelihood": {
+ "level": "low",
+ "rationale": "Requires local/shared filesystem placement and user selection."
+ },
+ "preconditions": [
+ "Attacker-controlled directory contents",
+ "Victim selects/restores directory"
+ ],
+ "reachability": {
+ "attacker": "Lower-privileged local/shared-filesystem actor.",
+ "entrypoint": "Game-directory activation and one-second local monitor.",
+ "evidenceRefs": [
+ "local-all-root-entries",
+ "local-task-per-id",
+ "local-index-whole-read",
+ "legacy-whole-read"
+ ],
+ "preconditions": [
+ "Attacker-controlled directory contents",
+ "Victim selects/restores directory"
+ ],
+ "summary": "Requires the operator to select or restore an attacker-influenced directory."
+ },
+ "summary": "A crafted selected directory or state file supplies extreme cardinality/depth/size, which periodic monitoring or startup migration consumes."
+ },
+ "codeEvidence": [
+ {
+ "code": "let mut games = BTreeMap::new();\nfor entry in fs::read_dir(game_dir) {\n ...\n games.insert(id.to_owned(), game_root);\n}",
+ "endLine": 235,
+ "explanation": "No catalog intersection or entry cap precedes retention.",
+ "id": "local-all-root-entries",
+ "label": "Every root name enters the snapshot",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/local_monitor.rs",
+ "role": "entrypoint",
+ "startLine": 200
+ },
+ {
+ "code": "for id in ready_ids {\n queue_rescan(ctx, tx_notify_ui, gate, rescans, id).await;\n}\n...\nrescans.spawn(async move { run_gated_rescan(...).await; });",
+ "endLine": 376,
+ "explanation": "Only duplicate work for the same ID coalesces; no global worker cap applies.",
+ "id": "local-task-per-id",
+ "label": "Every changed ID spawns a task",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/services/local_monitor.rs",
+ "role": "sink",
+ "startLine": 329
+ },
+ {
+ "code": "let data = match std::fs::read_to_string(path) { ... };\nmatch serde_json::from_str(&data) { ... }",
+ "endLine": 284,
+ "explanation": "A tampered app-state file can allocate arbitrarily.",
+ "id": "local-index-whole-read",
+ "label": "Current index is read without a byte cap",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/local_games.rs",
+ "role": "evidence",
+ "startLine": 268
+ },
+ {
+ "code": "let data = fs::read(legacy_path)?;\nwrite_bytes_atomically(target_path, &data)?;\nremove_file_if_exists(legacy_path)?;",
+ "endLine": 297,
+ "explanation": "Selection automatically crosses this legacy input boundary.",
+ "id": "legacy-whole-read",
+ "label": "Legacy migration duplicates an unbounded file",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/migration.rs",
+ "role": "evidence",
+ "startLine": 284
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "Unbounded enumeration, task spawn, WalkDir traversal, and whole-file reads are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_b4156302e37bcb0e5a87f4af",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:57f045ce125ace9a603ada98f96c92eb5c248a29cbe75d763ea9ee8beaccc919"
+ },
+ "identity": {
+ "anchor": "selected-game-root-ingestion-lacks-aggregate-budgets"
+ },
+ "locations": [
+ {
+ "endLine": 235,
+ "path": "crates/lanspread-peer/src/services/local_monitor.rs",
+ "role": "entrypoint",
+ "startLine": 200
+ },
+ {
+ "endLine": 376,
+ "path": "crates/lanspread-peer/src/services/local_monitor.rs",
+ "role": "sink",
+ "startLine": 329
+ },
+ {
+ "endLine": 294,
+ "path": "crates/lanspread-peer/src/local_games.rs",
+ "role": "sink",
+ "startLine": 268
+ },
+ {
+ "endLine": 297,
+ "path": "crates/lanspread-peer/src/migration.rs",
+ "role": "sink",
+ "startLine": 284
+ }
+ ],
+ "occurrenceId": "occ_396ac8c331a26fedd06ad199",
+ "provenance": {
+ "candidateId": "cand-local-library-budget",
+ "originalCandidates": [
+ {
+ "title": "Local-library ingestion budgets"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Intersect names with the catalog before retention/task admission; add root/per-game/depth/metadata budgets and a bounded rescan pool; bounded-read current and legacy indexes/version.ini and avoid raw invalid-content logging.",
+ "rootCause": {
+ "evidenceRefs": [
+ "local-all-root-entries",
+ "local-task-per-id",
+ "local-index-whole-read",
+ "legacy-whole-read"
+ ],
+ "summary": "Local filesystem inputs have shape/link checks but no aggregate cardinality, traversal, task, or persisted-file byte budgets."
+ },
+ "ruleId": "resource-exhaustion.local-library-ingestion",
+ "severity": {
+ "changeConditions": "Severity increases when selected game roots are writable by lower-privileged or remote filesystem users.",
+ "level": "low",
+ "rationale": "The desktop can be exhausted, but the attacker needs local/shared/removable-directory control plus operator selection; remote peers cannot create this fan-out."
+ },
+ "summary": "The continuously monitored game root retains every top-level name, spawns one task per changed ID, recursively walks trees, and reads current/legacy state files without byte budgets.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "Selected game directories can trigger unbounded monitoring and migration work",
+ "validation": {
+ "assertions": [
+ "Polling occurs every second.",
+ "All root names are retained before catalog filtering.",
+ "Per-ID task fan-out is not globally bounded.",
+ "Index/migration reads allocate complete files."
+ ],
+ "counterEvidence": [
+ "Symlinks/reparse points and special files are rejected.",
+ "Repeated work for one ID coalesces.",
+ "Remote catalog downloads cannot create arbitrary top-level entries."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "local-all-root-entries",
+ "local-task-per-id",
+ "local-index-whole-read",
+ "legacy-whole-read"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated and merged with the legacy-index candidate. Severity is low because meaningful influence is local/shared-root rather than ordinary LAN peer input."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "Safe argv handling inside Rust publisher",
+ "Benign defaults"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "just-raw-games-dir",
+ "just-raw-fixture-args",
+ "just-raw-production-args"
+ ],
+ "outcome": "Arbitrary command execution and catalog/build compromise.",
+ "sink": "Catalog/build shell with operator privileges.",
+ "source": "Operator-supplied recipe values.",
+ "summary": "just argument/environment -> `{{...}}` template -> bash recipe text -> command substitution/quote break -> arbitrary command."
+ },
+ "impact": {
+ "level": "high",
+ "rationale": "Build secrets and trusted catalog/bundle inputs can be modified or exfiltrated."
+ },
+ "likelihood": {
+ "level": "medium",
+ "rationale": "Documented commands accept paths, but operators often choose them themselves."
+ },
+ "preconditions": [
+ "Attacker-influenced just argument or LANSPREAD_UNRAR/GAMES_DIR",
+ "Operator executes recipe"
+ ],
+ "reachability": {
+ "attacker": "Package/path supplier or CI input actor.",
+ "entrypoint": "catalog/build/run just recipes.",
+ "evidenceRefs": [
+ "just-raw-games-dir",
+ "just-raw-fixture-args",
+ "just-raw-production-args"
+ ],
+ "preconditions": [
+ "Attacker-influenced just argument or LANSPREAD_UNRAR/GAMES_DIR",
+ "Operator executes recipe"
+ ],
+ "summary": "Conditional on an operator or CI passing an attacker-influenced value."
+ },
+ "summary": "A crafted path/selector/env value containing shell syntax is supplied to a documented recipe and evaluated by bash."
+ },
+ "codeEvidence": [
+ {
+ "code": "default:\n #!/usr/bin/env bash\n if [ -n \"{{GAMES_DIR}}\" ]; then\n just run-production \"{{GAMES_DIR}}\"\n else\n just run-fixture\n fi",
+ "endLine": 31,
+ "explanation": "Double quotes do not prevent shell command substitution after just renders the recipe.",
+ "id": "just-raw-games-dir",
+ "label": "GAMES_DIR is embedded in shell text",
+ "language": "text",
+ "path": "justfile",
+ "role": "root_control",
+ "startLine": 22
+ },
+ {
+ "code": "catalog-generate-fixture OUTPUT GAME_ROOT GAME_ID:\n cargo run ... --output \"{{OUTPUT}}\" --game-root \"{{GAME_ROOT}}\" --game-id \"{{GAME_ID}}\" --unrar \"{{CATALOG_UNRAR}}\"",
+ "endLine": 118,
+ "explanation": "Quote-breaking or `$()` reaches the recipe shell before the CLI.",
+ "id": "just-raw-fixture-args",
+ "label": "Fixture parameters are embedded directly",
+ "language": "text",
+ "path": "justfile",
+ "role": "evidence",
+ "startLine": 105
+ },
+ {
+ "code": "catalog-generate-production PACKAGES_DIR:\n #!/usr/bin/env bash\n if ... --packages-dir \"{{PACKAGES_DIR}}\" ... --unrar \"{{CATALOG_UNRAR}}\"; then ... fi",
+ "endLine": 170,
+ "explanation": "All cache/publisher invocations share the injection surface.",
+ "id": "just-raw-production-args",
+ "label": "Production values repeat across a shell conditional",
+ "language": "text",
+ "path": "justfile",
+ "role": "sink",
+ "startLine": 140
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "The raw templates and a harmless `just --dry-run` with literal command-substitution syntax confirm the rendered shell grammar."
+ },
+ "extensions": {},
+ "findingId": "csf_52309c6d49d62ae81258872c",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:612aaf874e672716f24d57db2b8b5793bf29242580683c9009791017da66b81c"
+ },
+ "identity": {
+ "anchor": "just-arguments-rendered-as-shell-source"
+ },
+ "locations": [
+ {
+ "endLine": 49,
+ "path": "justfile",
+ "role": "root_control",
+ "startLine": 22
+ },
+ {
+ "endLine": 118,
+ "path": "justfile",
+ "role": "sink",
+ "startLine": 105
+ },
+ {
+ "endLine": 170,
+ "path": "justfile",
+ "role": "sink",
+ "startLine": 140
+ }
+ ],
+ "occurrenceId": "occ_9293ad354e62522b50537a56",
+ "provenance": {
+ "candidateId": "cand-justfile-shell-interpolation",
+ "originalCandidates": [
+ {
+ "title": "justfile shell interpolation"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Pass data through environment/argv to non-shell helpers or apply just's correct shell-quoting facility to every argument and environment-derived value; test `$()`, quotes, whitespace, leading dashes, and newlines.",
+ "rootCause": {
+ "evidenceRefs": [
+ "just-raw-games-dir",
+ "just-raw-fixture-args",
+ "just-raw-production-args"
+ ],
+ "summary": "just template interpolation is used as shell quoting even though interpolation happens before shell parsing."
+ },
+ "ruleId": "command-injection.justfile-interpolation",
+ "severity": {
+ "changeConditions": "Severity increases in automated CI or workflows that pass externally supplied paths/IDs directly to just.",
+ "level": "medium",
+ "rationale": "Successful exploitation executes with catalog/build operator authority, but requires attacker influence over an operator-supplied recipe argument or environment value."
+ },
+ "summary": "Documented just recipes embed path, selector, output, and environment-derived values directly into shell source; command substitution and quote-breaking run before Rust/Python argv parsing.",
+ "taxonomy": {
+ "category": "command-injection",
+ "cwe": [
+ "CWE-78"
+ ]
+ },
+ "title": "Catalog/build recipe arguments are interpolated as shell code",
+ "validation": {
+ "assertions": [
+ "Arguments are rendered into recipe source.",
+ "The shell parses command substitution inside double quotes.",
+ "Rust subprocess construction is safe but occurs after this boundary."
+ ],
+ "counterEvidence": [
+ "Checked-in defaults and ordinary absolute paths are benign.",
+ "Exploitation needs attacker influence over operator arguments/environment, not merely package filenames."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "just-raw-games-dir",
+ "just-raw-fixture-args",
+ "just-raw-production-args"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated. `just --dry-run catalog-generate-production '$(printf SAFE_DRY_RUN_MARKER)'` preserved `$()` inside the rendered shell command; an actual shell would evaluate it."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "64 KiB version limit",
+ "Final-component link/reparse check",
+ "Unix inode/device comparison",
+ "Later root validation"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "publisher-preflight-order",
+ "publisher-content-error",
+ "publisher-windows-identity"
+ ],
+ "outcome": "Bounded out-of-root disclosure; Windows hash path may expose a digest oracle.",
+ "sink": "Publisher memory and stderr/CI log.",
+ "source": "Attacker-controlled package tree links/races.",
+ "summary": "packages_dir/game_id path -> joined version.ini -> path-based lstat/open -> UTF-8 contents -> error log."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "Can expose bounded sensitive content but cannot publish a stable linked package."
+ },
+ "likelihood": {
+ "level": "low",
+ "rationale": "Requires specially placed links or a filesystem race and often log visibility."
+ },
+ "preconditions": [
+ "Readable target named version.ini or race target",
+ "Access to resulting diagnostics for disclosure"
+ ],
+ "reachability": {
+ "attacker": "Package-corpus supplier or concurrent local process.",
+ "entrypoint": "Catalog generation version preflight.",
+ "evidenceRefs": [
+ "publisher-preflight-order",
+ "publisher-content-error",
+ "publisher-windows-identity"
+ ],
+ "preconditions": [
+ "Readable target named version.ini or race target",
+ "Access to resulting diagnostics for disclosure"
+ ],
+ "summary": "Requires publisher execution on an attacker-controlled package tree; Windows race is platform-specific."
+ },
+ "summary": "A package supplier points a selected game root outside the corpus or races a Windows file after lstat so publisher reads a different target."
+ },
+ "codeEvidence": [
+ {
+ "code": "for game in &selected {\n package::validate_package_version(\n &options.packages_dir.join(&game.game_id),\n &game.game_version,\n )?;\n}",
+ "endLine": 130,
+ "explanation": "The later manifest builder's non-link directory validation has not run.",
+ "id": "publisher-preflight-order",
+ "label": "Version read precedes root validation",
+ "language": "rust",
+ "path": "crates/lanspread-compat/src/catalog_publisher/mod.rs",
+ "role": "root_control",
+ "startLine": 120
+ },
+ {
+ "code": "let path = package_root.join(\"version.ini\");\nlet bytes = read_bounded_regular_file(&path, MAX_VERSION_INI_BYTES)?;\nlet version = std::str::from_utf8(&bytes)?.trim();\nif version != expected {\n eyre::bail!(\"... version.ini contains {version:?}\");\n}",
+ "endLine": 137,
+ "explanation": "Ancestor links are followed by the path open; mismatching content reaches stderr.",
+ "id": "publisher-content-error",
+ "label": "Out-of-root bytes are echoed on mismatch",
+ "language": "rust",
+ "path": "crates/lanspread-compat/src/catalog_publisher/package.rs",
+ "role": "sink",
+ "startLine": 125
+ },
+ {
+ "code": "#[cfg(not(unix))]\nfn same_file(_before: &fs::Metadata, _after: &fs::Metadata) -> bool {\n true\n}\n...\n#[cfg(windows)]\nfn is_link_or_reparse(metadata: &fs::Metadata) -> bool { ... }",
+ "endLine": 425,
+ "explanation": "The pre-open reparse check is not bound to the opened handle on Windows.",
+ "id": "publisher-windows-identity",
+ "label": "Windows accepts every check/open identity",
+ "language": "rust",
+ "path": "crates/lanspread-compat/src/catalog_publisher/package.rs",
+ "role": "root_control",
+ "startLine": 403
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "Ordering, whole diagnostic, and non-Unix `same_file=true` are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_5079213437e3f254ae70f2ca",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:a4df8ff5972c151be57355357b646ab9cd4276992c8021cc8257d35d60a9f80a"
+ },
+ "identity": {
+ "anchor": "package-preflight-not-rooted-to-nofollow-handle"
+ },
+ "locations": [
+ {
+ "endLine": 130,
+ "path": "crates/lanspread-compat/src/catalog_publisher/mod.rs",
+ "role": "root_control",
+ "startLine": 120
+ },
+ {
+ "endLine": 137,
+ "path": "crates/lanspread-compat/src/catalog_publisher/package.rs",
+ "role": "sink",
+ "startLine": 125
+ },
+ {
+ "endLine": 425,
+ "path": "crates/lanspread-compat/src/catalog_publisher/package.rs",
+ "role": "root_control",
+ "startLine": 403
+ }
+ ],
+ "occurrenceId": "occ_97c865ee39aa6ae912efe24f",
+ "provenance": {
+ "candidateId": "cand-package-root-link-preflight",
+ "originalCandidates": [
+ {
+ "title": "Publisher link preflight"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Open the packages/game roots through retained no-follow handles and open `version.ini` relative to them; implement Windows file-ID/reparse-safe identity checks; never print raw untrusted version contents.",
+ "rootCause": {
+ "evidenceRefs": [
+ "publisher-preflight-order",
+ "publisher-content-error",
+ "publisher-windows-identity"
+ ],
+ "summary": "Package reads are path-based rather than rooted in retained no-follow directory handles, and cross-platform object identity is not enforced."
+ },
+ "ruleId": "path-traversal.catalog-preflight-links",
+ "severity": {
+ "changeConditions": "Severity increases when publisher logs are visible to package suppliers or the build account has sensitive readable version.ini files.",
+ "level": "low",
+ "rationale": "The read is bounded and publication later fails; disclosure requires package-tree control and access to operator/CI diagnostics, with Windows race prerequisites for the TOCTOU variant."
+ },
+ "summary": "Version preflight joins `package_root/version.ini` before validating the game root, and Windows disables file-identity comparison, allowing ancestor-link traversal or a raced reparse target to be read and echoed.",
+ "taxonomy": {
+ "category": "path-traversal",
+ "cwe": [
+ "CWE-59",
+ "CWE-367"
+ ]
+ },
+ "title": "Catalog preflight can read outside the package root through links and Windows races",
+ "validation": {
+ "assertions": [
+ "Ancestor symlink validation is absent before preflight.",
+ "Final-component checks do not prove ancestor confinement.",
+ "Windows same_file unconditionally succeeds.",
+ "Mismatch errors include contents."
+ ],
+ "counterEvidence": [
+ "Stable final-component links/reparse points are rejected.",
+ "Read is capped at 64 KiB.",
+ "Later manifest building rejects a stable linked root.",
+ "Unix final-component swaps are device/inode checked."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "publisher-preflight-order",
+ "publisher-content-error",
+ "publisher-windows-identity"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated and merged with the Windows TOCTOU candidate. Publication fail-closed behavior limits the impact to bounded disclosure/digest-oracle and provenance violation."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "Catalog validation for downloaded files",
+ "Fixed unrar executable/argv",
+ "Transactional staging",
+ "Link/reparse rejection"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "eti-all-root-files",
+ "eti-promote-unverified",
+ "eti-native-extract"
+ ],
+ "outcome": "Uncatalogued installed bytes and decompression resource exhaustion.",
+ "sink": "Installed `local/` tree and native extraction resources.",
+ "source": "Locally/shared-root controlled regular `.eti` file.",
+ "summary": "Selected game root files -> extension enumeration -> native unrar -> staging -> local promotion."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "Installed content integrity and host availability are affected; direct remote or outside-root write is not established."
+ },
+ "likelihood": {
+ "level": "medium",
+ "rationale": "Requires local/shared-directory placement but extraction is deterministic after user action."
+ },
+ "preconditions": [
+ "Catalog game root with version.ini",
+ "Attacker-controlled extra `.eti`",
+ "Victim install/update"
+ ],
+ "reachability": {
+ "attacker": "Local/shared-filesystem actor.",
+ "entrypoint": "InstallGame/Update path after catalog ID and version sentinel checks.",
+ "evidenceRefs": [
+ "eti-all-root-files",
+ "eti-promote-unverified",
+ "eti-native-extract"
+ ],
+ "preconditions": [
+ "Catalog game root with version.ini",
+ "Attacker-controlled extra `.eti`",
+ "Victim install/update"
+ ],
+ "summary": "Requires write influence over the selected game root and local install/update action."
+ },
+ "summary": "A crafted unknown `.eti` is placed beside an otherwise catalog game; the victim invokes install/update; all archives are extracted and promoted."
+ },
+ "codeEvidence": [
+ {
+ "code": "let archives = root_eti_archives(game_root)?;\n...\nfor archive in archives {\n unpacker.unpack(&archive, staging, cancel_token.clone()).await?;\n}\n...\nif path.extension().is_some_and(|extension| extension == \"eti\") {\n archives.push(path);\n}",
+ "endLine": 562,
+ "explanation": "No local manifest archive-set, size, or digest check precedes native parsing.",
+ "id": "eti-all-root-files",
+ "label": "Every root `.eti` is selected",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/install/transaction.rs",
+ "role": "root_control",
+ "startLine": 514
+ },
+ {
+ "code": "prepare_owned_empty_dir(&staging)?;\nunpack_archives(game_root, &staging, unpacker, cancel_token).await?;\nrename_path(&staging, &local)?;",
+ "endLine": 468,
+ "explanation": "Ordinary install does not receive or verify the catalog's extracted-output manifest.",
+ "id": "eti-promote-unverified",
+ "label": "Staging is promoted after extractor success only",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/install/transaction.rs",
+ "role": "evidence",
+ "startLine": 449
+ },
+ {
+ "code": "ScopedProcess::spawn(\n program,\n [\"x\", \"-p-\", paths.archive.as_os_str(), \"-y\", \"-o\", &paths.destination_arg],\n ®istration.cancel_token(),\n UNRAR_LOG_CAPTURE_LIMIT,\n)",
+ "endLine": 3206,
+ "explanation": "The executable is fixed, but archive bytes and decompression cost cross a native parser boundary.",
+ "id": "eti-native-extract",
+ "label": "Tauri runs native extraction into staging",
+ "language": "rust",
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "sink",
+ "startLine": 3195
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "Archive enumeration, extraction loop, promotion, and absence of a manifest argument/check are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_2e096654b094e0fac1b124cd",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:effedb3e055f82da4175c26119809a0af75499bf7de3c2418c62b0b4ef3e0777"
+ },
+ "identity": {
+ "anchor": "ordinary-install-extracts-all-root-eti-files"
+ },
+ "locations": [
+ {
+ "endLine": 562,
+ "path": "crates/lanspread-peer/src/install/transaction.rs",
+ "role": "root_control",
+ "startLine": 449
+ },
+ {
+ "endLine": 3206,
+ "path": "crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs",
+ "role": "sink",
+ "startLine": 3195
+ },
+ {
+ "endLine": 1488,
+ "path": "crates/lanspread-peer/src/handlers.rs",
+ "role": "entrypoint",
+ "startLine": 1443
+ }
+ ],
+ "occurrenceId": "occ_4db1454c427333bbd2c39100",
+ "provenance": {
+ "candidateId": "cand-ordinary-install-uncatalogued-eti",
+ "originalCandidates": [
+ {
+ "title": "Uncatalogued ETI extraction"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Pass the exact catalog manifest into install/update; require the exact root archive set and verify each archive size/BLAKE3 through no-follow handles; verify every extracted path/kind/size/digest before promotion; enforce entry/decompressed-byte/total deadlines.",
+ "rootCause": {
+ "evidenceRefs": [
+ "eti-all-root-files",
+ "eti-promote-unverified",
+ "eti-native-extract"
+ ],
+ "summary": "The ordinary install path predates the catalog-owned archive/output contract used by Stream Install and treats filename extension plus extractor success as content authority."
+ },
+ "ruleId": "integrity.ordinary-install-uncatalogued-archive",
+ "severity": {
+ "changeConditions": "Severity increases if unverified extracted binaries are later executed by elevated catalog scripts or if the selected game directory is remotely writable.",
+ "level": "medium",
+ "rationale": "A crafted selected/shared game root can bypass installed-content integrity and exhaust disk/CPU; exploitation needs local/shared-root influence and a user install/update action."
+ },
+ "summary": "Install/update enumerates every root regular `.eti` file\u2014including unknown files deliberately preserved beside downloads\u2014runs native extraction for each, and promotes the staging tree without comparing the archive set or extracted output to the local catalog.",
+ "taxonomy": {
+ "category": "content-integrity",
+ "cwe": [
+ "CWE-494",
+ "CWE-400"
+ ]
+ },
+ "title": "Ordinary install extracts uncatalogued root archives without output verification",
+ "validation": {
+ "assertions": [
+ "Install admission can proceed with an untracked but sentinel-bearing root.",
+ "Every root `.eti` is extracted.",
+ "No extracted-output verification occurs before promotion."
+ ],
+ "counterEvidence": [
+ "Catalog-downloaded archive bytes are chunk-hash verified.",
+ "Symlink/reparse roots are rejected.",
+ "The sidecar path and argv grammar are fixed.",
+ "Outside-root archive behavior of opaque unrar was not claimed."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "eti-all-root-files",
+ "eti-promote-unverified",
+ "eti-native-extract"
+ ],
+ "method": "Independent static source trace against the registered revision and strongest counterevidence.",
+ "status": "validated",
+ "summary": "Validated. Remote downloads cannot add unknown files, but the selected/shared filesystem is an explicit untrusted boundary and unknown files are preserved by download ownership."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "4096-event limit",
+ "4 MiB author snapshot limit",
+ "retention windows"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "ctp-render-event-cap",
+ "ctp-render-clock",
+ "ctp-render-all-rows"
+ ],
+ "outcome": "Unresponsive launcher.",
+ "sink": "Main renderer CPU, heap, and DOM.",
+ "source": "Attacker-controlled valid Create events.",
+ "summary": "Pinned author snapshot -> Rust CallToPlayView -> reducer -> one-second recomputation -> full ticker/overlay render."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "UI availability can be lost without native compromise."
+ },
+ "likelihood": {
+ "level": "high",
+ "rationale": "One attacker identity can generate the valid event set."
+ },
+ "reachability": {
+ "attacker": "Authenticated hostile LAN peer.",
+ "entrypoint": "HelloSnapshot Call-to-Play slice.",
+ "evidenceRefs": [
+ "ctp-render-event-cap"
+ ],
+ "preconditions": [
+ "Sharing enabled",
+ "Valid 4096-event author snapshot"
+ ],
+ "summary": "Requires one connected hostile peer while sharing is enabled; no local click is needed after synchronization."
+ },
+ "summary": "A hostile peer publishes a maximal valid active-call slice that drives repeated full reduction and DOM reconciliation."
+ },
+ "codeEvidence": [
+ {
+ "code": "pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;\npub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;",
+ "endLine": 20,
+ "explanation": "The wire cap is not a smaller semantic active-call/display cap.",
+ "id": "ctp-render-event-cap",
+ "label": "A single author may publish 4096 events",
+ "language": "rust",
+ "path": "crates/lanspread-proto/src/lib.rs",
+ "role": "source",
+ "startLine": 17
+ },
+ {
+ "code": "const timer = window.setInterval(() => {\n setNow(Date.now());\n}, 1_000);",
+ "endLine": 72,
+ "explanation": "`now` is a dependency of the complete call reduction.",
+ "id": "ctp-render-clock",
+ "label": "Full reduction reruns each second",
+ "language": "typescript",
+ "path": "crates/lanspread-tauri-deno-ts/src/hooks/useCallToPlay.ts",
+ "role": "root_control",
+ "startLine": 68
+ },
+ {
+ "code": "const rows = nominations.map(nomination => ({ nomination, status: tickerStatusOf(nomination, now) })).sort(...);\n...\n{rows.map(({ nomination, status }) => {",
+ "endLine": 94,
+ "explanation": "There is no pagination, virtualization, or visible-row cap in the always-mounted ticker.",
+ "id": "ctp-render-all-rows",
+ "label": "Ticker renders every nomination",
+ "language": "typescript",
+ "path": "crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx",
+ "role": "sink",
+ "startLine": 80
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "The semantic validation, one-second recomputation, and full list rendering are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_a094c3abe43e8a361f0e7f84",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:0720a2a98628af9fcddc5bcbaffb29db6a8dbaef7bf4b32bdd6eb76635d7ced9"
+ },
+ "identity": {
+ "anchor": "maximal-active-calls-rendered-every-second"
+ },
+ "locations": [
+ {
+ "endLine": 20,
+ "path": "crates/lanspread-proto/src/lib.rs",
+ "role": "root_control",
+ "startLine": 17
+ },
+ {
+ "endLine": 72,
+ "path": "crates/lanspread-tauri-deno-ts/src/hooks/useCallToPlay.ts",
+ "role": "root_control",
+ "startLine": 68
+ },
+ {
+ "endLine": 154,
+ "path": "crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx",
+ "role": "sink",
+ "startLine": 80
+ }
+ ],
+ "occurrenceId": "occ_83d9017253563f8852490859",
+ "provenance": {
+ "candidateId": "cand-ctp-active-call-rendering",
+ "originalCandidates": [
+ {
+ "attacker": "One hostile authenticated LAN peer.",
+ "confidence": "high",
+ "cwe": "CWE-400",
+ "impact": "UI CPU/memory exhaustion and an unusable desktop renderer.",
+ "locations": [
+ "proto/lib.rs:17-20",
+ "call_to_play.rs:944-1039",
+ "frontend callToPlay.ts:125-146",
+ "useCallToPlay.ts:68-72,248-250",
+ "CallToPlayTicker.tsx:80-154",
+ "CallToPlayOverlay.tsx:38-95",
+ "NominationCard.tsx:139-247"
+ ],
+ "severity": "medium",
+ "source_to_sink": "A valid 4,096-event author slice can contain thousands of active Create roots; the frontend reduces/sorts the full set every second and renders every nomination in the always-mounted ticker and overlay.",
+ "title": "A hostile peer can force continuous rendering of thousands of active Call-to-Play entries"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Enforce small per-author/global active-call caps before publication, render only a ranked ticker subset, virtualize/paginate the overlay, and avoid recomputing unchanged projections on every clock tick.",
+ "rootCause": {
+ "evidenceRefs": [
+ "ctp-render-event-cap",
+ "ctp-render-clock",
+ "ctp-render-all-rows"
+ ],
+ "summary": "Wire-level event/byte limits are used as the only semantic and rendering budget; active roots and DOM rows have no smaller cap."
+ },
+ "ruleId": "resource-exhaustion.call-to-play-rendering",
+ "severity": {
+ "level": "medium",
+ "rationale": "One authenticated LAN peer can freeze the renderer without further user interaction, but protocol byte/event limits bound the state."
+ },
+ "summary": "A valid 4,096-event author slice can contain thousands of simultaneous Create roots; the frontend reduces and sorts the full set every second and renders every nomination in the always-mounted ticker.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "One peer can force continuous rendering of thousands of active calls",
+ "validation": {
+ "assertions": [
+ "4096 valid Create events can represent 4096 active calls.",
+ "Full reduction reruns each second.",
+ "All nominations are rendered."
+ ],
+ "counterEvidence": [
+ "Snapshot size and event count are finite.",
+ "Expired/terminal retention exists.",
+ "Per-card participant count is bounded."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "ctp-render-event-cap",
+ "ctp-render-clock",
+ "ctp-render-all-rows"
+ ],
+ "method": "Static source trace of maximal valid author state through the Rust projection and frontend render loop.",
+ "status": "validated",
+ "summary": "Validated as distinct from aggregate Sybil retention: one identity and one maximal valid slice are sufficient."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "64-peer cap",
+ "one attempt per identity",
+ "integrity quarantine",
+ "user cancellation"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "retry-all-identities",
+ "retry-transport-requeues",
+ "retry-ten-minute-attempt"
+ ],
+ "outcome": "Multi-hour denial of a game download.",
+ "sink": "Active download transaction, preallocated storage, and network tasks.",
+ "source": "Attacker-controlled PeerIds/endpoints.",
+ "summary": "Sybil availability claims -> exact-content source vector -> sequential retry batches -> fresh ten-minute deadline per identity."
+ },
+ "impact": {
+ "level": "medium",
+ "rationale": "One game operation can be held for many hours and consumes resources."
+ },
+ "likelihood": {
+ "level": "medium",
+ "rationale": "Requires many identities plus a local user-started download."
+ },
+ "reachability": {
+ "attacker": "Malicious same-LAN host.",
+ "entrypoint": "Remote library snapshots and ordinary chunk transport.",
+ "evidenceRefs": [
+ "retry-all-identities"
+ ],
+ "preconditions": [
+ "User starts download",
+ "Up to 64 Sybil peers claim exact ContentId",
+ "Sources stall/fail transport"
+ ],
+ "summary": "Requires a user-started download and many authenticated Sybil sources claiming the expected local catalog ContentId."
+ },
+ "summary": "One host advertises the exact ContentId under many identities and stalls each sequential chunk attempt."
+ },
+ "codeEvidence": [
+ {
+ "code": "/// Retries failed chunks against every eligible, nonquarantined peer identity.\n/// Each source is attempted at most once per chunk. There is no numeric retry cap.\n...\nwhile !queue.is_empty() { ... }",
+ "endLine": 327,
+ "explanation": "Identity multiplicity directly controls total operation duration.",
+ "id": "retry-all-identities",
+ "label": "Retries exhaust the complete source set",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/download/retry.rs",
+ "role": "root_control",
+ "startLine": 294
+ },
+ {
+ "code": "match kind {\n DownloadTransferErrorKind::Integrity | DownloadTransferErrorKind::Transport => {\n retry.last_error = error;\n queue.push_back(retry);\n }\n ...\n}",
+ "endLine": 219,
+ "explanation": "A stalling source is not quarantined and advances to the next identity.",
+ "id": "retry-transport-requeues",
+ "label": "Transport stalls are retryable",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/download/retry.rs",
+ "role": "root_control",
+ "startLine": 192
+ },
+ {
+ "code": "const ORDINARY_CHUNK_TRANSFER_TIMEOUT: Duration = Duration::from_mins(10);",
+ "endLine": 34,
+ "explanation": "Sixty-four distinct identities can multiply the per-attempt deadline to about 640 minutes.",
+ "id": "retry-ten-minute-attempt",
+ "label": "Each source gets a fresh ten-minute deadline",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/download/transport.rs",
+ "role": "sink",
+ "startLine": 34
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "The full-source retry loop, retry classification, peer cap, and per-attempt deadline are explicit."
+ },
+ "extensions": {},
+ "findingId": "csf_1c204082cf65f3b51769f582",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:73147d89e16ed49c48a790b8e6bb11fd0b5d7733f568fca8b60abb87a597f57a"
+ },
+ "identity": {
+ "anchor": "per-source-deadline-multiplied-by-sybil-sources"
+ },
+ "locations": [
+ {
+ "endLine": 327,
+ "path": "crates/lanspread-peer/src/download/retry.rs",
+ "role": "root_control",
+ "startLine": 294
+ },
+ {
+ "endLine": 34,
+ "path": "crates/lanspread-peer/src/download/transport.rs",
+ "role": "sink",
+ "startLine": 34
+ },
+ {
+ "endLine": 667,
+ "path": "crates/lanspread-peer/src/peer_db.rs",
+ "role": "source",
+ "startLine": 654
+ }
+ ],
+ "occurrenceId": "occ_80bdddc3df1afd907ea7579a",
+ "provenance": {
+ "candidateId": "cand-sybil-download-retry-budget",
+ "originalCandidates": [
+ {
+ "attacker": "One LAN host controlling many authenticated identities that advertise the exact catalog ContentId.",
+ "confidence": "high",
+ "cwe": "CWE-400",
+ "impact": "A user-started chunk can remain active for roughly 640 minutes across 64 identities.",
+ "locations": [
+ "handlers.rs:126-139",
+ "peer_db.rs:22,654-667",
+ "download/retry.rs:29-53,82-129,192-219,294-327",
+ "download/transport.rs:34",
+ "identity.rs:81-103",
+ "tls.rs:50-73"
+ ],
+ "severity": "medium",
+ "source_to_sink": "Retry state attempts every distinct identity with no numeric or total deadline; transport stalls are retryable and each source receives a fresh ten-minute chunk deadline.",
+ "title": "Sybil content sources multiply the per-chunk deadline into a multi-hour retry loop"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Add total per-chunk/download wall-clock and attempt budgets independent of source count, cap automatically tried identities, add per-origin identity quotas, and require explicit user retry after budget exhaustion.",
+ "rootCause": {
+ "evidenceRefs": [
+ "retry-all-identities",
+ "retry-transport-requeues",
+ "retry-ten-minute-attempt"
+ ],
+ "summary": "The retry budget is defined per identity rather than per chunk/download, while identities are cheap and attacker-multipliable."
+ },
+ "ruleId": "resource-exhaustion.download-retry-total-budget",
+ "severity": {
+ "level": "medium",
+ "rationale": "A hostile LAN host can hold a user-started operation for roughly 640 minutes, but the source count is finite and cancellation remains available."
+ },
+ "summary": "A download retries every distinct peer identity with no numeric or total deadline; transport stalls remain retryable and each of up to 64 Sybil sources receives a fresh ten-minute chunk deadline.",
+ "taxonomy": {
+ "category": "resource-exhaustion",
+ "cwe": [
+ "CWE-400"
+ ]
+ },
+ "title": "Sybil sources can multiply one chunk deadline into a multi-hour retry loop",
+ "validation": {
+ "assertions": [
+ "Up to 64 exact-content claimants enter the source set.",
+ "Every identity can be attempted once.",
+ "Transport stalls requeue.",
+ "Each attempt gets ten minutes."
+ ],
+ "counterEvidence": [
+ "Source count is capped at 64.",
+ "Integrity failures quarantine the peer/content pair.",
+ "User cancellation drains children and rolls back."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "retry-all-identities",
+ "retry-transport-requeues",
+ "retry-ten-minute-attempt"
+ ],
+ "method": "Static trace of source selection, transport failure policy, retry termination, and per-attempt deadline.",
+ "status": "validated",
+ "summary": "Validated; exact content verification prevents corrupt acceptance but not operation-lifetime exhaustion."
+ }
+ },
+ {
+ "attackPath": {
+ "controls": [
+ "Current internal catalog/single-component checks",
+ "Fixed marker basename"
+ ],
+ "dataflow": {
+ "evidenceRefs": [
+ "state-path-raw-join",
+ "state-path-public-write",
+ "state-path-sink"
+ ],
+ "outcome": "Fixed marker file creation/overwrite.",
+ "sink": "Filesystem outside state_dir.",
+ "source": "Embedding application input.",
+ "summary": "Raw game_id -> public path helper -> escaped PathBuf -> create_dir_all/write."
+ },
+ "impact": {
+ "level": "low",
+ "rationale": "Write target basename is fixed, but directories/path can escape."
+ },
+ "likelihood": {
+ "level": "low",
+ "rationale": "Current workspace callers prevalidate; exploitation depends on another caller."
+ },
+ "reachability": {
+ "attacker": "Caller controlling a game_id consumed by an embedding application.",
+ "entrypoint": "mark_launch_settings_applied or exported path helpers.",
+ "evidenceRefs": [
+ "state-path-public-write"
+ ],
+ "preconditions": [
+ "Embedding caller forwards untrusted identifier"
+ ],
+ "summary": "Public Rust API boundary; no shipping LAN route was found."
+ },
+ "summary": "An embedding caller forwards an untrusted game_id to the public marker API."
+ },
+ "codeEvidence": [
+ {
+ "code": "pub(crate) fn game_state_dir(state_dir: &Path, game_id: &str) -> PathBuf {\n games_state_dir(state_dir).join(game_id)\n}\n...\npub fn launch_settings_applied_path(state_dir: &Path, game_id: &str) -> PathBuf {\n game_state_dir(state_dir, game_id).join(LAUNCH_SETTINGS_APPLIED_FILE)\n}",
+ "endLine": 59,
+ "explanation": "Absolute and parent components are not rejected before Path::join.",
+ "id": "state-path-raw-join",
+ "label": "Raw game ID selects the state child",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/state_paths.rs",
+ "role": "root_control",
+ "startLine": 44
+ },
+ {
+ "code": "pub fn mark_launch_settings_applied(state_dir: &Path, game_id: &str) -> eyre::Result<()> {\n scoped_blocking(|| mark_applied(&launch_settings_applied_path(state_dir, game_id)))\n}",
+ "endLine": 175,
+ "explanation": "The public function documents no validated-ID precondition.",
+ "id": "state-path-public-write",
+ "label": "Public API writes the escaped marker",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/launch_settings.rs",
+ "role": "entrypoint",
+ "startLine": 168
+ },
+ {
+ "code": "if let Some(parent) = marker.parent() {\n std::fs::create_dir_all(parent)?;\n}\nstd::fs::write(marker, [])?;",
+ "endLine": 389,
+ "explanation": "The escaped path drives mutation with application privileges.",
+ "id": "state-path-sink",
+ "label": "Parents are created and marker overwritten",
+ "language": "rust",
+ "path": "crates/lanspread-peer/src/launch_settings.rs",
+ "role": "sink",
+ "startLine": 383
+ }
+ ],
+ "confidence": {
+ "level": "high",
+ "rationale": "The raw joins, public re-exports, and filesystem write are direct."
+ },
+ "extensions": {},
+ "findingId": "csf_8fefa5cb607eaa71c76a60ae",
+ "fingerprints": {
+ "algorithm": "codex-security/v1",
+ "primary": "codex-security/v1:sha256:0a3fdc1b7078165af877d4c58e0c2168c9f3911c3ae9a5086ef452dda147be52"
+ },
+ "identity": {
+ "anchor": "raw-game-id-joined-into-public-state-marker-path"
+ },
+ "locations": [
+ {
+ "endLine": 59,
+ "path": "crates/lanspread-peer/src/state_paths.rs",
+ "role": "root_control",
+ "startLine": 44
+ },
+ {
+ "endLine": 175,
+ "path": "crates/lanspread-peer/src/launch_settings.rs",
+ "role": "entrypoint",
+ "startLine": 168
+ },
+ {
+ "endLine": 389,
+ "path": "crates/lanspread-peer/src/launch_settings.rs",
+ "role": "sink",
+ "startLine": 383
+ }
+ ],
+ "occurrenceId": "occ_7a4292dcad47ad7e3e8412ef",
+ "provenance": {
+ "candidateId": "cand-public-state-path-traversal",
+ "originalCandidates": [
+ {
+ "attacker": "Untrusted game_id passed by an embedding caller.",
+ "confidence": "high",
+ "cwe": "CWE-22",
+ "impact": "Directory creation and fixed-basename file overwrite outside state_dir.",
+ "locations": [
+ "state_paths.rs:44-59",
+ "lib.rs:118-135",
+ "launch_settings.rs:168-175,383-389"
+ ],
+ "severity": "low",
+ "source_to_sink": "Public state path helpers Path::join raw game_id; mark_launch_settings_applied creates parents and writes a fixed marker at the resulting path.",
+ "title": "Public per-game state helpers accept traversal and absolute game IDs"
+ }
+ ],
+ "previousFindings": [],
+ "source": "focused_investigator"
+ },
+ "remediation": "Require a validated single-component GameId newtype or make helpers fallible and reject absolute/prefix/dot/parent/separator/reserved forms; write relative to a retained no-follow state-directory handle.",
+ "rootCause": {
+ "evidenceRefs": [
+ "state-path-raw-join",
+ "state-path-public-write",
+ "state-path-sink"
+ ],
+ "summary": "The public API represents game IDs as raw strings instead of a validated single-component type."
+ },
+ "ruleId": "path-traversal.public-state-path-helper",
+ "severity": {
+ "level": "low",
+ "rationale": "The write is constrained to a fixed basename and shipping callers validate/catalog-source IDs, but an embedding caller forwarding untrusted IDs crosses a real public library boundary."
+ },
+ "summary": "Public per-game path and marker APIs join an unvalidated game ID; absolute or parent-containing IDs escape `state_dir/games`, after which the marker writer creates parents and overwrites a fixed file.",
+ "taxonomy": {
+ "category": "path-traversal",
+ "cwe": [
+ "CWE-22"
+ ]
+ },
+ "title": "Public state helpers permit marker writes outside the state directory",
+ "validation": {
+ "assertions": [
+ "Absolute child replaces the accumulated PathBuf prefix.",
+ "Parent components escape when resolved.",
+ "Public marker API creates parents and writes."
+ ],
+ "counterEvidence": [
+ "Windows run-game checks one component.",
+ "Internal StreamInstall calls use catalog IDs.",
+ "Only a fixed basename is written."
+ ],
+ "disposition": "reportable",
+ "evidenceRefs": [
+ "state-path-raw-join",
+ "state-path-public-write",
+ "state-path-sink"
+ ],
+ "method": "Static public-API trace from caller-controlled identifier through Path::join to filesystem write.",
+ "status": "validated",
+ "summary": "Validated with low severity; no direct LAN route exists and current shipping callers use validated IDs."
+ }
+ }
+ ],
+ "scanId": "916f0812-d79c-465f-a422-81d75a258f76",
+ "schemaVersion": "1.0"
+}
diff --git a/security-report/report.md b/security-report/report.md
new file mode 100644
index 0000000..125dc72
--- /dev/null
+++ b/security-report/report.md
@@ -0,0 +1,3656 @@
+# Security Review: lanspread
+
+## Scope
+
+Whole Git-backed Lanspread repository at revision 15faecbf89735f277f208c84895464a5190b9236, including Rust workspace crates, Tauri frontend/shell, catalog/build tooling, peer CLI, tests, configs, generated permission manifests, and opaque sidecars as data.
+
+- Scan mode: repository
+- Target kind: git_revision
+- Target ID: target_sha256_b3f00671b54800aee0df5478bf1bd03061b69b79eb9beece1ed34dc8c8bbc8a6
+- Revision: 15faecbf89735f277f208c84895464a5190b9236
+- Inventory strategy: repository
+- Included paths: .
+- Excluded paths: none
+- Runtime or test status: Source review, validation, attack paths, and coverage reconciliation complete.
+- Artifacts reviewed: 94 Rust source files, production TypeScript/TSX/CSS/HTML frontend, Cargo/deno manifests and locks, Tauri capabilities/config/build gate, just/Python/Docker publisher and peer-CLI workflows, tests exposing product contracts, opaque unrar sidecar metadata/digests
+- Scan context: Standard single-pass read-only security audit with no additional user focus.
+
+Limitations and exclusions:
+- Offline static review only; no application or third-party binary execution.
+- No online dependency advisory lookup.
+- External production package corpus and generated production manifests absent.
+- No physical-LAN, Windows UAC, or live resource-exhaustion reproduction.
+- Excluded crates/lanspread-tauri-deno-ts/src-tauri/assets/\*\* and src-tauri/icons/\*\*: Static image/icon payloads; file types and consumers were checked, but pixel data is not executable security logic.
+- Excluded crates/lanspread-peer-cli/fixtures/\*\* and catalog fixture payload bytes (\*.eti, \*.ini, \*.db): Static test corpus. Publisher/loaders, catalog manifests, protocol harness, and scenario consumers were reviewed rather than treating every fixture byte as implementation source.
+- Excluded crates/lanspread-tauri-deno-ts/src-tauri/gen/schemas/\*\* except ACL/capability manifests: Generated schema output; checked-in capability source and generated ACL/capability manifests owning runtime permissions were inspected.
+- Excluded Documentation-only design prose outside README.md and crates/lanspread-peer/ARCHITECTURE.md: Non-executable prose not owning a runtime/build control; security claims in the primary product/architecture docs were traced to consumers.
+
+### Scan Summary
+
+| Field | Value |
+| --- | --- |
+| Scan outcome | completed |
+| Reportable findings | 15 |
+| Severity mix | medium: 10, low: 5 |
+| Confidence mix | high: 15 |
+| Coverage | partial |
+| Validation mode | Every worker candidate checkpointed before one parent source validation; duplicates merged only when root cause and remediation matched. |
+
+Canonical artifacts: `scan-manifest.json`, `findings.json`, and `coverage.json`. This report is a deterministic projection of those files.
+
+## Threat Model
+
+Lanspread is a single-user LAN-party game-sharing desktop app plus a developer JSONL CLI and catalog publication tools. A sharing-enabled peer binds ephemeral IPv4 QUIC on all interfaces, advertises/discovers via mDNS, authenticates only the responder with SPKI-derived PeerId pinning, and serves requester-anonymous current-protocol operations. Remote availability and bytes remain subordinate to the bundled catalog. Sources: README.md:3-17; crates/lanspread-peer/src/tls.rs:51-143; crates/lanspread-peer/src/services/server.rs:74-177; crates/lanspread-peer/src/download/manifest.rs:129-185.
+
+### Assets
+
+- Installation Ed25519 private key and stable PeerId in Tauri AppData or CLI state-dir. Sources: crates/lanspread-peer/src/state_paths.rs:18-36; identity.rs:61-165,223-291.
+- Catalog authority: game.db, compact content index, canonical manifest paths/sizes/BLAKE3 and Stream Install outputs. Sources: crates/lanspread-compat/src/catalog_bundle.rs:50-82,169-233; crates/lanspread-db/src/content_manifest/bundle.rs:20-124.
+- Configured game storage, user-owned local installs, version sentinel, staging/backup, install intent, and download ownership journals. Sources: crates/lanspread-peer/ARCHITECTURE.md:202-260.
+- Authenticated endpoint generations, runtime sessions, library/Call-to-Play projections, and application availability.
+- Tauri native command/plugin authority and Windows administrator process boundary.
+- Catalog package/build provenance and checked-in opaque unrar sidecars.
+
+### Trust Boundaries
+
+- Anonymous LAN requester -\> QUIC server: TLS authenticates the responder only; global frame/task/transfer controls and sharing state govern admission. Sources: tls.rs:126-143; services/server.rs:31-365; services/stream.rs:34-303.
+- mDNS hint -\> pinned endpoint: claimed PeerId/address occupies bounded candidate work; only a pinned full pull commits state. Sources: services/discovery.rs:30-61,233-259; services/remote_state.rs:147-275.
+- Pinned snapshot -\> peer database and Call-to-Play: endpoint generation, runtime session, revision and independent domain validation gate commit.
+- Remote availability/bytes -\> local catalog -\> confined filesystem: peers never choose expected paths/hashes; complete catalog manifests and no-follow capabilities gate reads/writes.
+- Stream Install frames -\> verified staging: catalog archive/output shape, size and digest must match before promotion; ordinary install is a materially different path.
+- Main/auxiliary Tauri webviews -\> registered native commands/plugins; bundled content and localhost development content both have CSP null.
+- User-selected game directory -\> backend canonical acknowledgement -\> scanner/downloader/installer/Windows launcher.
+- Catalog/package operator -\> publisher shell recipes -\> Rust publisher/unrar -\> production manifest/bundle authority.
+- Catalog or mutable game scripts -\> Windows cmd.exe runas after local action/UAC.
+- AppData/state-dir -\> identity, policy, logs, migration and recovery consumers.
+
+### Attacker Capabilities
+
+- Same-LAN host may advertise arbitrary identities/endpoints, operate valid self-issued identities, send anonymous requests/hints, and control its own snapshots/transfer bytes, but not forge another PeerId or local catalog hashes.
+- Local/shared-filesystem actor may populate a user-selected games/package directory but does not initially control AppData, build operator credentials, or victim UAC decision.
+- Local CLI stdin controller is operator authority and is not modeled as a remote attacker.
+- Compromised renderer is conditional; no current hostile-peer XSS or remote-navigation route was established.
+- Package/CI input supplier may influence documented recipe arguments where an operator passes them through.
+
+### Security Objectives
+
+- Pin every outbound responder to the expected Ed25519 SPKI-derived PeerId and TLS 1.3 proof.
+- When sharing is disabled, close admission, drain network generations, and clear remote projections.
+- Treat mDNS, hints and payload claims as non-authoritative until a pinned pull and current generation/session commit.
+- Accept only protocol 8 with strict bounded decoding and no compatibility fallback.
+- Keep remote bytes subordinate to local catalog ContentId/path/size/BLAKE3 authority.
+- Confine file reads/writes/deletes to canonical game roots and preserve unknown/user-owned content outside explicit operations.
+- Bound public work before allocation and apply fair quotas to attacker-multipliable identities/origins.
+- Keep fixture authority out of production builds and make catalog publication fail closed.
+- Bind elevated process launch to verified immutable content.
+- Keep process/task shutdown structured and joined.
+
+### Assumptions
+
+- Requester anonymity while Local network sharing is enabled is intentional product behavior; responder identity, not membership, is authenticated.
+- Missing sharing policy defaults to enabled, while malformed/unreadable policy fails disabled and no game directory prevents peer startup.
+- The current production game.db has 186 rows but the production manifests and external package corpus are absent, so their aggregate size/provenance cannot be verified.
+- Tauri AppData/Resource absolute paths are platform-owned; logical child paths are source-backed.
+- Windows UAC policy, firewall/LAN segmentation, physical-LAN behavior and cross-user directory ACLs are deployment prerequisites.
+- Checked-in unrar binaries were inspected as opaque binary data and hashed, but source-to-binary/upstream provenance is unresolved.
+- Docker peer CLI and Python scenario tools are developer/test surfaces, not production requester APIs.
+- No source-backed frontend script injection, raw HTML, eval, or remote navigation path was found.
+- Windows game/server scripts are elevated by source but this privilege transition is not documented in README/ARCHITECTURE.
+
+## Findings
+
+| Finding | Severity | Confidence | Detailed write-up |
+| --- | --- | --- | --- |
+| [Anonymous LAN requesters can monopolize global pools and native extractors](#finding-1) | medium | high | inline below |
+| [Rejected bulk requests can populate the persistent manifest cache](#finding-2) | medium | high | inline below |
+| [Ordinary install extracts uncatalogued root archives without output verification](#finding-3) | medium | high | inline below |
+| [Sybil sources can multiply one chunk deadline into a multi-hour retry loop](#finding-4) | medium | high | inline below |
+| [One peer can force continuous rendering of thousands of active calls](#finding-5) | medium | high | inline below |
+| [Control-frame prefixes can reserve about 512 MiB across concurrent decoders](#finding-6) | medium | high | inline below |
+| [Catalog/build recipe arguments are interpolated as shell code](#finding-7) | medium | high | inline below |
+| [Sybil peers can exhaust aggregate state and unbounded UI publication](#finding-8) | medium | high | inline below |
+| [Unauthenticated hints can make the victim pull arbitrary known peers](#finding-9) | medium | high | inline below |
+| [Mutable game scripts are launched elevated without launch-time trust binding](#finding-10) | medium | high | inline below |
+| [Selected game directories can trigger unbounded monitoring and migration work](#finding-11) | low | high | inline below |
+| [Public state helpers permit marker writes outside the state directory](#finding-12) | low | high | inline below |
+| [Catalog preflight can read outside the package root through links and Windows races](#finding-13) | low | high | inline below |
+| [Cross-author event-ID collisions can suppress Call-to-Play entries](#finding-14) | low | high | inline below |
+| [Forged mDNS candidates can monopolize discovery slots](#finding-15) | low | high | inline below |
+
+### Confidence Scale
+
+| Label | Meaning |
+| --- | --- |
+| high | Direct evidence supports the finding with no material unresolved blocker. |
+| medium | Evidence supports a plausible issue, but material runtime or reachability proof remains. |
+| low | Evidence is incomplete and the item is retained only for explicit follow-up. |
+
+
+
+### [1] Anonymous LAN requesters can monopolize global pools and native extractors
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | No-client-auth, global-only semaphores, request dispatch, and per-request provider execution are explicit. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-peer/src/tls.rs:126-133, crates/lanspread-peer/src/services/server.rs:31-44, crates/lanspread-peer/src/services/stream.rs:70-89, crates/lanspread-peer/src/stream_install.rs:387-414 |
+
+#### Summary
+
+The responder authenticates no client and applies only global connection, control, and bulk limits, so one origin can occupy every admission slot and drive up to 48 concurrent StreamInstall operations with native unrar work.
+
+#### Root Cause
+
+Anonymous requester admission is protected only by finite global pools; expensive StreamInstall provider work has no smaller global or per-origin quota.
+
+**Server accepts anonymous requesters** — `crates/lanspread-peer/src/tls.rs:126-133`
+
+Requester identity cannot support fair per-peer quotas.
+
+```rust
+let mut config = ServerConfig::builder_with_provider(provider)
+ .with_protocol_versions(&[&rustls::version::TLS13])?
+ .with_no_client_auth()
+ .with_single_cert(...)?;
+```
+
+**Admission budgets are global** — `crates/lanspread-peer/src/services/server.rs:31-44`
+
+One origin can consume every shared slot.
+
+```rust
+const MAX_ESTABLISHED_CONNECTIONS: usize = 64;
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+const MAX_GLOBAL_BULK_TRANSFER_TASKS: usize = 48;
+```
+
+**Each bulk request independently takes the shared pool** — `crates/lanspread-peer/src/services/stream.rs:70-89`
+
+There is no source-address, requester, game, or expensive-provider quota.
+
+```rust
+let bulk_permit = Arc::clone(&bulk_transfer_permits).try_acquire_owned();
+drop(control_permit.take());
+if let Ok(permit) = bulk_permit {
+ _bulk_permit = Some(permit);
+ ...
+}
+```
+
+**Stream Install starts native archive processing** — `crates/lanspread-peer/src/stream_install.rs:387-414`
+
+Each admitted operation can start native listing and later decompression.
+
+```rust
+let process = ScopedProcess::spawn(
+ program,
+ ["vt", "-c-", "-p-", ...],
+ cancel_token,
+ LISTING_CAPTURE_LIMIT,
+)?;
+```
+
+#### Validation
+
+Validated; the separate StreamInstall-subprocess candidate is merged here because it shares anonymous global bulk admission and the same quota remediation.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Server accepts anonymous requesters** — `crates/lanspread-peer/src/tls.rs:126-133`
+
+Requester identity cannot support fair per-peer quotas.
+
+```rust
+let mut config = ServerConfig::builder_with_provider(provider)
+ .with_protocol_versions(&[&rustls::version::TLS13])?
+ .with_no_client_auth()
+ .with_single_cert(...)?;
+```
+
+**Admission budgets are global** — `crates/lanspread-peer/src/services/server.rs:31-44`
+
+One origin can consume every shared slot.
+
+```rust
+const MAX_ESTABLISHED_CONNECTIONS: usize = 64;
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+const MAX_GLOBAL_BULK_TRANSFER_TASKS: usize = 48;
+```
+
+**Each bulk request independently takes the shared pool** — `crates/lanspread-peer/src/services/stream.rs:70-89`
+
+There is no source-address, requester, game, or expensive-provider quota.
+
+```rust
+let bulk_permit = Arc::clone(&bulk_transfer_permits).try_acquire_owned();
+drop(control_permit.take());
+if let Ok(permit) = bulk_permit {
+ _bulk_permit = Some(permit);
+ ...
+}
+```
+
+**Stream Install starts native archive processing** — `crates/lanspread-peer/src/stream_install.rs:387-414`
+
+Each admitted operation can start native listing and later decompression.
+
+```rust
+let process = ScopedProcess::spawn(
+ program,
+ ["vt", "-c-", "-p-", ...],
+ cancel_token,
+ LISTING_CAPTURE_LIMIT,
+)?;
+```
+
+Assertions:
+- No client identity exists.
+- All resource permits are shared globally.
+- StreamInstall performs native work after acquiring only the general bulk permit.
+
+Counterevidence and remaining uncertainty:
+- Handshake, connection, control, and bulk counts are finite.
+- Control and bulk pools are separated.
+- Stalled work has application timeouts and cancellation.
+
+#### Dataflow
+
+Anonymous QUIC connection -\> global stream/bulk permit -\> request handler -\> file handles or unrar subprocesses.
+
+- **Source:** Same-LAN client requests.
+
+- **Sink:** Global peer admission, bulk transfer capacity, native subprocess/CPU resources.
+
+- **Outcome:** Denial of legitimate sync/download/install service.
+
+**Server accepts anonymous requesters** — `crates/lanspread-peer/src/tls.rs:126-133`
+
+Requester identity cannot support fair per-peer quotas.
+
+```rust
+let mut config = ServerConfig::builder_with_provider(provider)
+ .with_protocol_versions(&[&rustls::version::TLS13])?
+ .with_no_client_auth()
+ .with_single_cert(...)?;
+```
+
+**Admission budgets are global** — `crates/lanspread-peer/src/services/server.rs:31-44`
+
+One origin can consume every shared slot.
+
+```rust
+const MAX_ESTABLISHED_CONNECTIONS: usize = 64;
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+const MAX_GLOBAL_BULK_TRANSFER_TASKS: usize = 48;
+```
+
+**Each bulk request independently takes the shared pool** — `crates/lanspread-peer/src/services/stream.rs:70-89`
+
+There is no source-address, requester, game, or expensive-provider quota.
+
+```rust
+let bulk_permit = Arc::clone(&bulk_transfer_permits).try_acquire_owned();
+drop(control_permit.take());
+if let Ok(permit) = bulk_permit {
+ _bulk_permit = Some(permit);
+ ...
+}
+```
+
+**Stream Install starts native archive processing** — `crates/lanspread-peer/src/stream_install.rs:387-414`
+
+Each admitted operation can start native listing and later decompression.
+
+```rust
+let process = ScopedProcess::spawn(
+ program,
+ ["vt", "-c-", "-p-", ...],
+ cancel_token,
+ LISTING_CAPTURE_LIMIT,
+)?;
+```
+
+#### Reachability
+
+Directly reachable whenever Local network sharing is enabled.
+
+- **Attacker:** Any host on the reachable LAN.
+
+- **Entry point:** QUIC listener and StreamInstall/GetGameFileChunk requests.
+
+Preconditions:
+- Sharing enabled
+- Victim advertises or attacker learns endpoint
+- For StreamInstall, a locally available stream-capable game
+
+Existing controls:
+- Finite global caps
+- 10-second control deadlines
+- 10-minute inactivity deadlines
+- Separate control and bulk pools
+
+**Server accepts anonymous requesters** — `crates/lanspread-peer/src/tls.rs:126-133`
+
+Requester identity cannot support fair per-peer quotas.
+
+```rust
+let mut config = ServerConfig::builder_with_provider(provider)
+ .with_protocol_versions(&[&rustls::version::TLS13])?
+ .with_no_client_auth()
+ .with_single_cert(...)?;
+```
+
+**Admission budgets are global** — `crates/lanspread-peer/src/services/server.rs:31-44`
+
+One origin can consume every shared slot.
+
+```rust
+const MAX_ESTABLISHED_CONNECTIONS: usize = 64;
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+const MAX_GLOBAL_BULK_TRANSFER_TASKS: usize = 48;
+```
+
+**Each bulk request independently takes the shared pool** — `crates/lanspread-peer/src/services/stream.rs:70-89`
+
+There is no source-address, requester, game, or expensive-provider quota.
+
+```rust
+let bulk_permit = Arc::clone(&bulk_transfer_permits).try_acquire_owned();
+drop(control_permit.take());
+if let Ok(permit) = bulk_permit {
+ _bulk_permit = Some(permit);
+ ...
+}
+```
+
+**Stream Install starts native archive processing** — `crates/lanspread-peer/src/stream_install.rs:387-414`
+
+Each admitted operation can start native listing and later decompression.
+
+```rust
+let process = ScopedProcess::spawn(
+ program,
+ ["vt", "-c-", "-p-", ...],
+ cancel_token,
+ LISTING_CAPTURE_LIMIT,
+)?;
+```
+
+#### Severity
+
+**Medium** — A same-LAN attacker can reliably deny synchronization and transfers and impose expensive native work, but all pools are finite and the attack must be sustained.
+
+Severity increases on memory-constrained hosts or archives with expensive decompression.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** Availability is lost and host resources are pressured; file integrity remains catalog-protected.
+
+Likelihood assessment:
+- **Level:** high
+- **Rationale:** Only ordinary LAN reachability and sustained requests are required.
+
+#### Remediation
+
+Add per-source connection/stream/rate quotas, close origins after repeated timeouts, reserve capacity across origins, place unrar behind a small global and per-origin semaphore, and enforce absolute transfer deadlines.
+
+
+
+### [2] Rejected bulk requests can populate the persistent manifest cache
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | Call order and unbounded cache insertion are direct; the missing production bodies only limit measurement. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-peer/src/services/transfer.rs:258-321, crates/lanspread-db/src/content_manifest/store.rs:162-186 |
+
+#### Summary
+
+Both bulk request variants load and retain a full catalog manifest before checking compact ContentId equality or whether the game is locally serveable.
+
+#### Root Cause
+
+Bulk admission uses the disk-capable on-demand manifest API before the compact index and preloaded local-library cache can reject the request.
+
+**Full body loads precede rejection** — `crates/lanspread-peer/src/services/transfer.rs:270-284`
+
+A wrong ContentId or nonlocal known game still causes a body load.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await { return None; }
+```
+
+**Stream Install repeats the ordering** — `crates/lanspread-peer/src/services/transfer.rs:308-319`
+
+Both independent network operations are affected.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await || !manifest.supports_streamed_install() { return None; }
+```
+
+**Every miss is retained** — `crates/lanspread-db/src/content_manifest/store.rs:162-186`
+
+No entry/byte eviction applies to cumulative remote-triggered loads.
+
+```rust
+let manifest = Arc::new(self.load_uncached(game_id, expected_version)?);
+let mut cache = self.cache.write()?;
+Ok(cache.entry(game_id.to_owned()).or_insert_with(|| Arc::clone(&manifest)).clone())
+```
+
+#### Validation
+
+Validated. Known IDs and per-body limits bound the universe, but do not enforce the documented no-I/O rejection or an aggregate retained-byte limit.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Full body loads precede rejection** — `crates/lanspread-peer/src/services/transfer.rs:270-284`
+
+A wrong ContentId or nonlocal known game still causes a body load.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await { return None; }
+```
+
+**Stream Install repeats the ordering** — `crates/lanspread-peer/src/services/transfer.rs:308-319`
+
+Both independent network operations are affected.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await || !manifest.supports_streamed_install() { return None; }
+```
+
+**Every miss is retained** — `crates/lanspread-db/src/content_manifest/store.rs:162-186`
+
+No entry/byte eviction applies to cumulative remote-triggered loads.
+
+```rust
+let manifest = Arc::new(self.load_uncached(game_id, expected_version)?);
+let mut cache = self.cache.write()?;
+Ok(cache.entry(game_id.to_owned()).or_insert_with(|| Arc::clone(&manifest)).clone())
+```
+
+Assertions:
+- The compact identity is available without filesystem I/O.
+- Both bulk variants call full load before identity/local checks.
+- Loaded bodies persist in a HashMap without eviction.
+
+Counterevidence and remaining uncertainty:
+- Only immutable catalog IDs can load.
+- Each manifest and catalog are finite.
+- Operation admission serializes loads.
+- Production bodies are absent, so actual aggregate size was not measured.
+
+#### Dataflow
+
+Network game_id -\> CatalogBundle::manifest -\> bounded disk read/parse -\> persistent cache -\> later rejection.
+
+- **Source:** Unauthenticated bulk request fields.
+
+- **Sink:** Process heap, manifest parser, and serialized operation-admission lock.
+
+- **Outcome:** Memory growth and denial/delay of legitimate operations.
+
+**Full body loads precede rejection** — `crates/lanspread-peer/src/services/transfer.rs:270-284`
+
+A wrong ContentId or nonlocal known game still causes a body load.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await { return None; }
+```
+
+**Stream Install repeats the ordering** — `crates/lanspread-peer/src/services/transfer.rs:308-319`
+
+Both independent network operations are affected.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await || !manifest.supports_streamed_install() { return None; }
+```
+
+**Every miss is retained** — `crates/lanspread-db/src/content_manifest/store.rs:162-186`
+
+No entry/byte eviction applies to cumulative remote-triggered loads.
+
+```rust
+let manifest = Arc::new(self.load_uncached(game_id, expected_version)?);
+let mut cache = self.cache.write()?;
+Ok(cache.entry(game_id.to_owned()).or_insert_with(|| Arc::clone(&manifest)).clone())
+```
+
+#### Reachability
+
+Reachable for any known bundled game ID while sharing is enabled.
+
+- **Attacker:** Same-LAN anonymous requester.
+
+- **Entry point:** GetGameFileChunk and StreamInstall.
+
+Preconditions:
+- Sharing enabled
+- Knowledge or enumeration of bundled game IDs
+
+Existing controls:
+- 128 MiB individual manifest limit
+- Finite immutable catalog
+- Serialized operation admission
+
+**Full body loads precede rejection** — `crates/lanspread-peer/src/services/transfer.rs:270-284`
+
+A wrong ContentId or nonlocal known game still causes a body load.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await { return None; }
+```
+
+**Stream Install repeats the ordering** — `crates/lanspread-peer/src/services/transfer.rs:308-319`
+
+Both independent network operations are affected.
+
+```rust
+let manifest = load_expected_catalog_manifest(ctx, game_id)?;
+if manifest.content_id() != content_id { return None; }
+if !can_serve_game(ctx, &game_dir, game_id).await || !manifest.supports_streamed_install() { return None; }
+```
+
+**Every miss is retained** — `crates/lanspread-db/src/content_manifest/store.rs:162-186`
+
+No entry/byte eviction applies to cumulative remote-triggered loads.
+
+```rust
+let manifest = Arc::new(self.load_uncached(game_id, expected_version)?);
+let mut cache = self.cache.write()?;
+Ok(cache.entry(game_id.to_owned()).or_insert_with(|| Arc::clone(&manifest)).clone())
+```
+
+#### Severity
+
+**Medium** — An anonymous LAN client can force persistent corpus-wide parse/retention and serialized admission work; actual production size is unavailable, limiting impact certainty.
+
+Severity depends on the aggregate encoded/parsed size of the external production manifest corpus.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** Potential process memory exhaustion and operational blocking; exact magnitude depends on external corpus.
+
+Likelihood assessment:
+- **Level:** high
+- **Rationale:** Wrong ContentIds are sufficient and no local availability is required.
+
+#### Remediation
+
+Check `content_identity` and local readiness first, then require `cached_manifest`; add tests proving rejected requests perform no load, plus an aggregate cache budget.
+
+
+
+### [3] Ordinary install extracts uncatalogued root archives without output verification
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | Archive enumeration, extraction loop, promotion, and absence of a manifest argument/check are explicit. |
+| Category | content-integrity |
+| CWE | CWE-494, CWE-400 |
+| Affected lines | crates/lanspread-peer/src/install/transaction.rs:449-562, crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3195-3206, crates/lanspread-peer/src/handlers.rs:1443-1488 |
+
+#### Summary
+
+Install/update enumerates every root regular `.eti` file—including unknown files deliberately preserved beside downloads—runs native extraction for each, and promotes the staging tree without comparing the archive set or extracted output to the local catalog.
+
+#### Root Cause
+
+The ordinary install path predates the catalog-owned archive/output contract used by Stream Install and treats filename extension plus extractor success as content authority.
+
+**Every root `.eti` is selected** — `crates/lanspread-peer/src/install/transaction.rs:514-562`
+
+No local manifest archive-set, size, or digest check precedes native parsing.
+
+```rust
+let archives = root_eti_archives(game_root)?;
+...
+for archive in archives {
+ unpacker.unpack(&archive, staging, cancel_token.clone()).await?;
+}
+...
+if path.extension().is_some_and(|extension| extension == "eti") {
+ archives.push(path);
+}
+```
+
+**Staging is promoted after extractor success only** — `crates/lanspread-peer/src/install/transaction.rs:449-468`
+
+Ordinary install does not receive or verify the catalog's extracted-output manifest.
+
+```rust
+prepare_owned_empty_dir(&staging)?;
+unpack_archives(game_root, &staging, unpacker, cancel_token).await?;
+rename_path(&staging, &local)?;
+```
+
+**Tauri runs native extraction into staging** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3195-3206`
+
+The executable is fixed, but archive bytes and decompression cost cross a native parser boundary.
+
+```rust
+ScopedProcess::spawn(
+ program,
+ ["x", "-p-", paths.archive.as_os_str(), "-y", "-o", &paths.destination_arg],
+ ®istration.cancel_token(),
+ UNRAR_LOG_CAPTURE_LIMIT,
+)
+```
+
+#### Validation
+
+Validated. Remote downloads cannot add unknown files, but the selected/shared filesystem is an explicit untrusted boundary and unknown files are preserved by download ownership.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Every root `.eti` is selected** — `crates/lanspread-peer/src/install/transaction.rs:514-562`
+
+No local manifest archive-set, size, or digest check precedes native parsing.
+
+```rust
+let archives = root_eti_archives(game_root)?;
+...
+for archive in archives {
+ unpacker.unpack(&archive, staging, cancel_token.clone()).await?;
+}
+...
+if path.extension().is_some_and(|extension| extension == "eti") {
+ archives.push(path);
+}
+```
+
+**Staging is promoted after extractor success only** — `crates/lanspread-peer/src/install/transaction.rs:449-468`
+
+Ordinary install does not receive or verify the catalog's extracted-output manifest.
+
+```rust
+prepare_owned_empty_dir(&staging)?;
+unpack_archives(game_root, &staging, unpacker, cancel_token).await?;
+rename_path(&staging, &local)?;
+```
+
+**Tauri runs native extraction into staging** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3195-3206`
+
+The executable is fixed, but archive bytes and decompression cost cross a native parser boundary.
+
+```rust
+ScopedProcess::spawn(
+ program,
+ ["x", "-p-", paths.archive.as_os_str(), "-y", "-o", &paths.destination_arg],
+ ®istration.cancel_token(),
+ UNRAR_LOG_CAPTURE_LIMIT,
+)
+```
+
+Assertions:
+- Install admission can proceed with an untracked but sentinel-bearing root.
+- Every root `.eti` is extracted.
+- No extracted-output verification occurs before promotion.
+
+Counterevidence and remaining uncertainty:
+- Catalog-downloaded archive bytes are chunk-hash verified.
+- Symlink/reparse roots are rejected.
+- The sidecar path and argv grammar are fixed.
+- Outside-root archive behavior of opaque unrar was not claimed.
+
+#### Dataflow
+
+Selected game root files -\> extension enumeration -\> native unrar -\> staging -\> local promotion.
+
+- **Source:** Locally/shared-root controlled regular `.eti` file.
+
+- **Sink:** Installed `local/` tree and native extraction resources.
+
+- **Outcome:** Uncatalogued installed bytes and decompression resource exhaustion.
+
+**Every root `.eti` is selected** — `crates/lanspread-peer/src/install/transaction.rs:514-562`
+
+No local manifest archive-set, size, or digest check precedes native parsing.
+
+```rust
+let archives = root_eti_archives(game_root)?;
+...
+for archive in archives {
+ unpacker.unpack(&archive, staging, cancel_token.clone()).await?;
+}
+...
+if path.extension().is_some_and(|extension| extension == "eti") {
+ archives.push(path);
+}
+```
+
+**Staging is promoted after extractor success only** — `crates/lanspread-peer/src/install/transaction.rs:449-468`
+
+Ordinary install does not receive or verify the catalog's extracted-output manifest.
+
+```rust
+prepare_owned_empty_dir(&staging)?;
+unpack_archives(game_root, &staging, unpacker, cancel_token).await?;
+rename_path(&staging, &local)?;
+```
+
+**Tauri runs native extraction into staging** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3195-3206`
+
+The executable is fixed, but archive bytes and decompression cost cross a native parser boundary.
+
+```rust
+ScopedProcess::spawn(
+ program,
+ ["x", "-p-", paths.archive.as_os_str(), "-y", "-o", &paths.destination_arg],
+ ®istration.cancel_token(),
+ UNRAR_LOG_CAPTURE_LIMIT,
+)
+```
+
+#### Reachability
+
+Requires write influence over the selected game root and local install/update action.
+
+- **Attacker:** Local/shared-filesystem actor.
+
+- **Entry point:** InstallGame/Update path after catalog ID and version sentinel checks.
+
+Preconditions:
+- Catalog game root with version.ini
+- Attacker-controlled extra `.eti`
+- Victim install/update
+
+Existing controls:
+- Catalog validation for downloaded files
+- Fixed unrar executable/argv
+- Transactional staging
+- Link/reparse rejection
+
+**Every root `.eti` is selected** — `crates/lanspread-peer/src/install/transaction.rs:514-562`
+
+No local manifest archive-set, size, or digest check precedes native parsing.
+
+```rust
+let archives = root_eti_archives(game_root)?;
+...
+for archive in archives {
+ unpacker.unpack(&archive, staging, cancel_token.clone()).await?;
+}
+...
+if path.extension().is_some_and(|extension| extension == "eti") {
+ archives.push(path);
+}
+```
+
+**Staging is promoted after extractor success only** — `crates/lanspread-peer/src/install/transaction.rs:449-468`
+
+Ordinary install does not receive or verify the catalog's extracted-output manifest.
+
+```rust
+prepare_owned_empty_dir(&staging)?;
+unpack_archives(game_root, &staging, unpacker, cancel_token).await?;
+rename_path(&staging, &local)?;
+```
+
+**Tauri runs native extraction into staging** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:3195-3206`
+
+The executable is fixed, but archive bytes and decompression cost cross a native parser boundary.
+
+```rust
+ScopedProcess::spawn(
+ program,
+ ["x", "-p-", paths.archive.as_os_str(), "-y", "-o", &paths.destination_arg],
+ ®istration.cancel_token(),
+ UNRAR_LOG_CAPTURE_LIMIT,
+)
+```
+
+#### Severity
+
+**Medium** — A crafted selected/shared game root can bypass installed-content integrity and exhaust disk/CPU; exploitation needs local/shared-root influence and a user install/update action.
+
+Severity increases if unverified extracted binaries are later executed by elevated catalog scripts or if the selected game directory is remotely writable.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** Installed content integrity and host availability are affected; direct remote or outside-root write is not established.
+
+Likelihood assessment:
+- **Level:** medium
+- **Rationale:** Requires local/shared-directory placement but extraction is deterministic after user action.
+
+#### Remediation
+
+Pass the exact catalog manifest into install/update; require the exact root archive set and verify each archive size/BLAKE3 through no-follow handles; verify every extracted path/kind/size/digest before promotion; enforce entry/decompressed-byte/total deadlines.
+
+
+
+### [4] Sybil sources can multiply one chunk deadline into a multi-hour retry loop
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | The full-source retry loop, retry classification, peer cap, and per-attempt deadline are explicit. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-peer/src/download/retry.rs:294-327, crates/lanspread-peer/src/download/transport.rs:34, crates/lanspread-peer/src/peer_db.rs:654-667 |
+
+#### Summary
+
+A download retries every distinct peer identity with no numeric or total deadline; transport stalls remain retryable and each of up to 64 Sybil sources receives a fresh ten-minute chunk deadline.
+
+#### Root Cause
+
+The retry budget is defined per identity rather than per chunk/download, while identities are cheap and attacker-multipliable.
+
+**Retries exhaust the complete source set** — `crates/lanspread-peer/src/download/retry.rs:294-327`
+
+Identity multiplicity directly controls total operation duration.
+
+```rust
+/// Retries failed chunks against every eligible, nonquarantined peer identity.
+/// Each source is attempted at most once per chunk. There is no numeric retry cap.
+...
+while !queue.is_empty() { ... }
+```
+
+**Transport stalls are retryable** — `crates/lanspread-peer/src/download/retry.rs:192-219`
+
+A stalling source is not quarantined and advances to the next identity.
+
+```rust
+match kind {
+ DownloadTransferErrorKind::Integrity | DownloadTransferErrorKind::Transport => {
+ retry.last_error = error;
+ queue.push_back(retry);
+ }
+ ...
+}
+```
+
+**Each source gets a fresh ten-minute deadline** — `crates/lanspread-peer/src/download/transport.rs:34`
+
+Sixty-four distinct identities can multiply the per-attempt deadline to about 640 minutes.
+
+```rust
+const ORDINARY_CHUNK_TRANSFER_TIMEOUT: Duration = Duration::from_mins(10);
+```
+
+#### Validation
+
+Validated; exact content verification prevents corrupt acceptance but not operation-lifetime exhaustion.
+
+Validation method: Static trace of source selection, transport failure policy, retry termination, and per-attempt deadline.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Retries exhaust the complete source set** — `crates/lanspread-peer/src/download/retry.rs:294-327`
+
+Identity multiplicity directly controls total operation duration.
+
+```rust
+/// Retries failed chunks against every eligible, nonquarantined peer identity.
+/// Each source is attempted at most once per chunk. There is no numeric retry cap.
+...
+while !queue.is_empty() { ... }
+```
+
+**Transport stalls are retryable** — `crates/lanspread-peer/src/download/retry.rs:192-219`
+
+A stalling source is not quarantined and advances to the next identity.
+
+```rust
+match kind {
+ DownloadTransferErrorKind::Integrity | DownloadTransferErrorKind::Transport => {
+ retry.last_error = error;
+ queue.push_back(retry);
+ }
+ ...
+}
+```
+
+**Each source gets a fresh ten-minute deadline** — `crates/lanspread-peer/src/download/transport.rs:34`
+
+Sixty-four distinct identities can multiply the per-attempt deadline to about 640 minutes.
+
+```rust
+const ORDINARY_CHUNK_TRANSFER_TIMEOUT: Duration = Duration::from_mins(10);
+```
+
+Assertions:
+- Up to 64 exact-content claimants enter the source set.
+- Every identity can be attempted once.
+- Transport stalls requeue.
+- Each attempt gets ten minutes.
+
+Counterevidence and remaining uncertainty:
+- Source count is capped at 64.
+- Integrity failures quarantine the peer/content pair.
+- User cancellation drains children and rolls back.
+
+#### Dataflow
+
+Sybil availability claims -\> exact-content source vector -\> sequential retry batches -\> fresh ten-minute deadline per identity.
+
+- **Source:** Attacker-controlled PeerIds/endpoints.
+
+- **Sink:** Active download transaction, preallocated storage, and network tasks.
+
+- **Outcome:** Multi-hour denial of a game download.
+
+**Retries exhaust the complete source set** — `crates/lanspread-peer/src/download/retry.rs:294-327`
+
+Identity multiplicity directly controls total operation duration.
+
+```rust
+/// Retries failed chunks against every eligible, nonquarantined peer identity.
+/// Each source is attempted at most once per chunk. There is no numeric retry cap.
+...
+while !queue.is_empty() { ... }
+```
+
+**Transport stalls are retryable** — `crates/lanspread-peer/src/download/retry.rs:192-219`
+
+A stalling source is not quarantined and advances to the next identity.
+
+```rust
+match kind {
+ DownloadTransferErrorKind::Integrity | DownloadTransferErrorKind::Transport => {
+ retry.last_error = error;
+ queue.push_back(retry);
+ }
+ ...
+}
+```
+
+**Each source gets a fresh ten-minute deadline** — `crates/lanspread-peer/src/download/transport.rs:34`
+
+Sixty-four distinct identities can multiply the per-attempt deadline to about 640 minutes.
+
+```rust
+const ORDINARY_CHUNK_TRANSFER_TIMEOUT: Duration = Duration::from_mins(10);
+```
+
+#### Reachability
+
+Requires a user-started download and many authenticated Sybil sources claiming the expected local catalog ContentId.
+
+- **Attacker:** Malicious same-LAN host.
+
+- **Entry point:** Remote library snapshots and ordinary chunk transport.
+
+Preconditions:
+- User starts download
+- Up to 64 Sybil peers claim exact ContentId
+- Sources stall/fail transport
+
+Existing controls:
+- 64-peer cap
+- one attempt per identity
+- integrity quarantine
+- user cancellation
+
+**Retries exhaust the complete source set** — `crates/lanspread-peer/src/download/retry.rs:294-327`
+
+Identity multiplicity directly controls total operation duration.
+
+```rust
+/// Retries failed chunks against every eligible, nonquarantined peer identity.
+/// Each source is attempted at most once per chunk. There is no numeric retry cap.
+...
+while !queue.is_empty() { ... }
+```
+
+#### Severity
+
+**Medium** — A hostile LAN host can hold a user-started operation for roughly 640 minutes, but the source count is finite and cancellation remains available.
+
+Additional runtime or deployment evidence could raise or lower this severity.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** One game operation can be held for many hours and consumes resources.
+
+Likelihood assessment:
+- **Level:** medium
+- **Rationale:** Requires many identities plus a local user-started download.
+
+#### Remediation
+
+Add total per-chunk/download wall-clock and attempt budgets independent of source count, cap automatically tried identities, add per-origin identity quotas, and require explicit user retry after budget exhaustion.
+
+
+
+### [5] One peer can force continuous rendering of thousands of active calls
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | The semantic validation, one-second recomputation, and full list rendering are explicit. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-proto/src/lib.rs:17-20, crates/lanspread-tauri-deno-ts/src/hooks/useCallToPlay.ts:68-72, crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx:80-154 |
+
+#### Summary
+
+A valid 4,096-event author slice can contain thousands of simultaneous Create roots; the frontend reduces and sorts the full set every second and renders every nomination in the always-mounted ticker.
+
+#### Root Cause
+
+Wire-level event/byte limits are used as the only semantic and rendering budget; active roots and DOM rows have no smaller cap.
+
+**A single author may publish 4096 events** — `crates/lanspread-proto/src/lib.rs:17-20`
+
+The wire cap is not a smaller semantic active-call/display cap.
+
+```rust
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+**Full reduction reruns each second** — `crates/lanspread-tauri-deno-ts/src/hooks/useCallToPlay.ts:68-72`
+
+`now` is a dependency of the complete call reduction.
+
+```typescript
+const timer = window.setInterval(() => {
+ setNow(Date.now());
+}, 1_000);
+```
+
+**Ticker renders every nomination** — `crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx:80-94`
+
+There is no pagination, virtualization, or visible-row cap in the always-mounted ticker.
+
+```typescript
+const rows = nominations.map(nomination => ({ nomination, status: tickerStatusOf(nomination, now) })).sort(...);
+...
+{rows.map(({ nomination, status }) => {
+```
+
+#### Validation
+
+Validated as distinct from aggregate Sybil retention: one identity and one maximal valid slice are sufficient.
+
+Validation method: Static source trace of maximal valid author state through the Rust projection and frontend render loop.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**A single author may publish 4096 events** — `crates/lanspread-proto/src/lib.rs:17-20`
+
+The wire cap is not a smaller semantic active-call/display cap.
+
+```rust
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+**Full reduction reruns each second** — `crates/lanspread-tauri-deno-ts/src/hooks/useCallToPlay.ts:68-72`
+
+`now` is a dependency of the complete call reduction.
+
+```typescript
+const timer = window.setInterval(() => {
+ setNow(Date.now());
+}, 1_000);
+```
+
+**Ticker renders every nomination** — `crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx:80-94`
+
+There is no pagination, virtualization, or visible-row cap in the always-mounted ticker.
+
+```typescript
+const rows = nominations.map(nomination => ({ nomination, status: tickerStatusOf(nomination, now) })).sort(...);
+...
+{rows.map(({ nomination, status }) => {
+```
+
+Assertions:
+- 4096 valid Create events can represent 4096 active calls.
+- Full reduction reruns each second.
+- All nominations are rendered.
+
+Counterevidence and remaining uncertainty:
+- Snapshot size and event count are finite.
+- Expired/terminal retention exists.
+- Per-card participant count is bounded.
+
+#### Dataflow
+
+Pinned author snapshot -\> Rust CallToPlayView -\> reducer -\> one-second recomputation -\> full ticker/overlay render.
+
+- **Source:** Attacker-controlled valid Create events.
+
+- **Sink:** Main renderer CPU, heap, and DOM.
+
+- **Outcome:** Unresponsive launcher.
+
+**A single author may publish 4096 events** — `crates/lanspread-proto/src/lib.rs:17-20`
+
+The wire cap is not a smaller semantic active-call/display cap.
+
+```rust
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+**Full reduction reruns each second** — `crates/lanspread-tauri-deno-ts/src/hooks/useCallToPlay.ts:68-72`
+
+`now` is a dependency of the complete call reduction.
+
+```typescript
+const timer = window.setInterval(() => {
+ setNow(Date.now());
+}, 1_000);
+```
+
+**Ticker renders every nomination** — `crates/lanspread-tauri-deno-ts/src/components/calltoplay/CallToPlayTicker.tsx:80-94`
+
+There is no pagination, virtualization, or visible-row cap in the always-mounted ticker.
+
+```typescript
+const rows = nominations.map(nomination => ({ nomination, status: tickerStatusOf(nomination, now) })).sort(...);
+...
+{rows.map(({ nomination, status }) => {
+```
+
+#### Reachability
+
+Requires one connected hostile peer while sharing is enabled; no local click is needed after synchronization.
+
+- **Attacker:** Authenticated hostile LAN peer.
+
+- **Entry point:** HelloSnapshot Call-to-Play slice.
+
+Preconditions:
+- Sharing enabled
+- Valid 4096-event author snapshot
+
+Existing controls:
+- 4096-event limit
+- 4 MiB author snapshot limit
+- retention windows
+
+**A single author may publish 4096 events** — `crates/lanspread-proto/src/lib.rs:17-20`
+
+The wire cap is not a smaller semantic active-call/display cap.
+
+```rust
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+#### Severity
+
+**Medium** — One authenticated LAN peer can freeze the renderer without further user interaction, but protocol byte/event limits bound the state.
+
+Additional runtime or deployment evidence could raise or lower this severity.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** UI availability can be lost without native compromise.
+
+Likelihood assessment:
+- **Level:** high
+- **Rationale:** One attacker identity can generate the valid event set.
+
+#### Remediation
+
+Enforce small per-author/global active-call caps before publication, render only a ranked ticker subset, virtualize/paginate the overlay, and avoid recomputing unchanged projections on every clock tick.
+
+
+
+### [6] Control-frame prefixes can reserve about 512 MiB across concurrent decoders
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | Repository concurrency/frame limits and the locked dependency's reserve behavior were inspected offline. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-proto/src/lib.rs:13-20, crates/lanspread-peer/src/services/server.rs:31-41, crates/lanspread-peer/src/services/stream.rs:37-67, crates/lanspread-peer/src/services/discovery.rs:30-32, Cargo.lock:4655-4659 |
+
+#### Summary
+
+The same 8 MiB control-frame allowance is multiplied across 64 concurrent inbound request decoders and 64 discovery Hello decoders; locked tokio-util eagerly reserves the declared body after only the length prefix.
+
+#### Root Cause
+
+A response-sized per-frame maximum is applied independently to many untrusted decoders without an aggregate receive-memory budget or incremental allocation.
+
+**Every control codec permits 8 MiB** — `crates/lanspread-proto/src/lib.rs:13-14`
+
+Inbound requests are much smaller, but share the response-sized maximum.
+
+```rust
+pub const MAX_CONTROL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+pub const MAX_STREAM_INSTALL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+```
+
+**Server admits 64 global decoders** — `crates/lanspread-peer/src/services/server.rs:31-41`
+
+Two connections can populate all 64 stream tasks.
+
+```rust
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+```
+
+**Decoder uses the shared maximum before parsing** — `crates/lanspread-peer/src/services/stream.rs:37-67`
+
+The whole declared body is awaited before Request::decode.
+
+```rust
+LengthDelimitedCodec::builder()
+ .max_frame_length(MAX_CONTROL_FRAME_BYTES)
+ .new_codec()
+...
+let first_frame = read_expected_frame(...).await;
+```
+
+**Locked decoder version** — `Cargo.lock:4655-4659`
+
+Offline inspection of this locked source confirmed `src.reserve(n.saturating_sub(src.len()))` after decoding the header.
+
+```text
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+```
+
+#### Validation
+
+Validated and merged with the discovery-prefix instance because both use the same locked decoder behavior and aggregate-budget failure.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Every control codec permits 8 MiB** — `crates/lanspread-proto/src/lib.rs:13-14`
+
+Inbound requests are much smaller, but share the response-sized maximum.
+
+```rust
+pub const MAX_CONTROL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+pub const MAX_STREAM_INSTALL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+```
+
+**Server admits 64 global decoders** — `crates/lanspread-peer/src/services/server.rs:31-41`
+
+Two connections can populate all 64 stream tasks.
+
+```rust
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+```
+
+**Decoder uses the shared maximum before parsing** — `crates/lanspread-peer/src/services/stream.rs:37-67`
+
+The whole declared body is awaited before Request::decode.
+
+```rust
+LengthDelimitedCodec::builder()
+ .max_frame_length(MAX_CONTROL_FRAME_BYTES)
+ .new_codec()
+...
+let first_frame = read_expected_frame(...).await;
+```
+
+**Locked decoder version** — `Cargo.lock:4655-4659`
+
+Offline inspection of this locked source confirmed `src.reserve(n.saturating_sub(src.len()))` after decoding the header.
+
+```text
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+```
+
+Assertions:
+- 64 multiplied by 8 MiB is 512 MiB before overhead.
+- Only four prefix bytes are needed before tokio-util reserves.
+- Inbound and discovery response paths both use the shared maximum.
+
+Counterevidence and remaining uncertainty:
+- Global task/candidate caps bound a single wave.
+- Ten-second deadlines release stalled decoders.
+- Discovery identities must authenticate as themselves, though inbound clients are anonymous.
+
+#### Dataflow
+
+Length prefix -\> LengthDelimitedCodec header decode -\> eager BytesMut reserve -\> incomplete frame wait.
+
+- **Source:** Unauthenticated inbound clients or attacker-operated discovery responders.
+
+- **Sink:** Desktop heap and every global control/discovery slot.
+
+- **Outcome:** Abrupt memory pressure, swapping, or process termination.
+
+**Every control codec permits 8 MiB** — `crates/lanspread-proto/src/lib.rs:13-14`
+
+Inbound requests are much smaller, but share the response-sized maximum.
+
+```rust
+pub const MAX_CONTROL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+pub const MAX_STREAM_INSTALL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+```
+
+**Server admits 64 global decoders** — `crates/lanspread-peer/src/services/server.rs:31-41`
+
+Two connections can populate all 64 stream tasks.
+
+```rust
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+```
+
+**Decoder uses the shared maximum before parsing** — `crates/lanspread-peer/src/services/stream.rs:37-67`
+
+The whole declared body is awaited before Request::decode.
+
+```rust
+LengthDelimitedCodec::builder()
+ .max_frame_length(MAX_CONTROL_FRAME_BYTES)
+ .new_codec()
+...
+let first_frame = read_expected_frame(...).await;
+```
+
+**Locked decoder version** — `Cargo.lock:4655-4659`
+
+Offline inspection of this locked source confirmed `src.reserve(n.saturating_sub(src.len()))` after decoding the header.
+
+```text
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+```
+
+#### Reachability
+
+Inbound requires listener access; discovery requires 64 advertised identities/endpoints with matching keys.
+
+- **Attacker:** Same-LAN participant.
+
+- **Entry point:** Server bidirectional streams and discovery Hello responses.
+
+Preconditions:
+- Sharing/discovery enabled
+- Concurrent streams or candidates
+
+Existing controls:
+- 64-decoder/candidate caps
+- 10-second application deadlines
+- QUIC transport limits
+
+**Every control codec permits 8 MiB** — `crates/lanspread-proto/src/lib.rs:13-14`
+
+Inbound requests are much smaller, but share the response-sized maximum.
+
+```rust
+pub const MAX_CONTROL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+pub const MAX_STREAM_INSTALL_FRAME_BYTES: usize = 8 * 1024 * 1024;
+```
+
+**Server admits 64 global decoders** — `crates/lanspread-peer/src/services/server.rs:31-41`
+
+Two connections can populate all 64 stream tasks.
+
+```rust
+const MAX_CONTROL_STREAM_TASKS: usize = 32;
+const MAX_GLOBAL_CONTROL_STREAM_TASKS: usize = 64;
+```
+
+**Decoder uses the shared maximum before parsing** — `crates/lanspread-peer/src/services/stream.rs:37-67`
+
+The whole declared body is awaited before Request::decode.
+
+```rust
+LengthDelimitedCodec::builder()
+ .max_frame_length(MAX_CONTROL_FRAME_BYTES)
+ .new_codec()
+...
+let first_frame = read_expected_frame(...).await;
+```
+
+**Locked decoder version** — `Cargo.lock:4655-4659`
+
+Offline inspection of this locked source confirmed `src.reserve(n.saturating_sub(src.len()))` after decoding the header.
+
+```text
+[[package]]
+name = "tokio-util"
+version = "0.7.19"
+checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
+```
+
+#### Severity
+
+**Medium** — A LAN attacker can induce abrupt half-gigabyte allocation pressure with negligible payload, but global caps and 10-second deadlines bound each wave.
+
+Severity increases on memory-constrained desktops or if concurrency/frame limits grow.
+
+Impact assessment:
+- **Level:** high
+- **Rationale:** A half-gigabyte allocation spike can terminate constrained desktops.
+
+Likelihood assessment:
+- **Level:** medium
+- **Rationale:** Inbound requires only two connections; discovery requires more setup. Waves are finite but repeatable.
+
+#### Remediation
+
+Use a much smaller inbound Request-frame maximum; gate declared response bytes through a global memory semaphore or incremental buffering; reduce discovery fan-out as defense in depth.
+
+
+
+### [7] Catalog/build recipe arguments are interpolated as shell code
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | The raw templates and a harmless `just --dry-run` with literal command-substitution syntax confirm the rendered shell grammar. |
+| Category | command-injection |
+| CWE | CWE-78 |
+| Affected lines | justfile:22-49, justfile:105-118, justfile:140-170 |
+
+#### Summary
+
+Documented just recipes embed path, selector, output, and environment-derived values directly into shell source; command substitution and quote-breaking run before Rust/Python argv parsing.
+
+#### Root Cause
+
+just template interpolation is used as shell quoting even though interpolation happens before shell parsing.
+
+**GAMES_DIR is embedded in shell text** — `justfile:22-31`
+
+Double quotes do not prevent shell command substitution after just renders the recipe.
+
+```text
+default:
+ #!/usr/bin/env bash
+ if [ -n "{{GAMES_DIR}}" ]; then
+ just run-production "{{GAMES_DIR}}"
+ else
+ just run-fixture
+ fi
+```
+
+**Fixture parameters are embedded directly** — `justfile:105-118`
+
+Quote-breaking or `$()` reaches the recipe shell before the CLI.
+
+```text
+catalog-generate-fixture OUTPUT GAME_ROOT GAME_ID:
+ cargo run ... --output "{{OUTPUT}}" --game-root "{{GAME_ROOT}}" --game-id "{{GAME_ID}}" --unrar "{{CATALOG_UNRAR}}"
+```
+
+**Production values repeat across a shell conditional** — `justfile:140-170`
+
+All cache/publisher invocations share the injection surface.
+
+```text
+catalog-generate-production PACKAGES_DIR:
+ #!/usr/bin/env bash
+ if ... --packages-dir "{{PACKAGES_DIR}}" ... --unrar "{{CATALOG_UNRAR}}"; then ... fi
+```
+
+#### Validation
+
+Validated. `just --dry-run catalog-generate-production '$(printf SAFE_DRY_RUN_MARKER)'` preserved `$()` inside the rendered shell command; an actual shell would evaluate it.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**GAMES_DIR is embedded in shell text** — `justfile:22-31`
+
+Double quotes do not prevent shell command substitution after just renders the recipe.
+
+```text
+default:
+ #!/usr/bin/env bash
+ if [ -n "{{GAMES_DIR}}" ]; then
+ just run-production "{{GAMES_DIR}}"
+ else
+ just run-fixture
+ fi
+```
+
+**Fixture parameters are embedded directly** — `justfile:105-118`
+
+Quote-breaking or `$()` reaches the recipe shell before the CLI.
+
+```text
+catalog-generate-fixture OUTPUT GAME_ROOT GAME_ID:
+ cargo run ... --output "{{OUTPUT}}" --game-root "{{GAME_ROOT}}" --game-id "{{GAME_ID}}" --unrar "{{CATALOG_UNRAR}}"
+```
+
+**Production values repeat across a shell conditional** — `justfile:140-170`
+
+All cache/publisher invocations share the injection surface.
+
+```text
+catalog-generate-production PACKAGES_DIR:
+ #!/usr/bin/env bash
+ if ... --packages-dir "{{PACKAGES_DIR}}" ... --unrar "{{CATALOG_UNRAR}}"; then ... fi
+```
+
+Assertions:
+- Arguments are rendered into recipe source.
+- The shell parses command substitution inside double quotes.
+- Rust subprocess construction is safe but occurs after this boundary.
+
+Counterevidence and remaining uncertainty:
+- Checked-in defaults and ordinary absolute paths are benign.
+- Exploitation needs attacker influence over operator arguments/environment, not merely package filenames.
+
+#### Dataflow
+
+just argument/environment -\> `{{...}}` template -\> bash recipe text -\> command substitution/quote break -\> arbitrary command.
+
+- **Source:** Operator-supplied recipe values.
+
+- **Sink:** Catalog/build shell with operator privileges.
+
+- **Outcome:** Arbitrary command execution and catalog/build compromise.
+
+**GAMES_DIR is embedded in shell text** — `justfile:22-31`
+
+Double quotes do not prevent shell command substitution after just renders the recipe.
+
+```text
+default:
+ #!/usr/bin/env bash
+ if [ -n "{{GAMES_DIR}}" ]; then
+ just run-production "{{GAMES_DIR}}"
+ else
+ just run-fixture
+ fi
+```
+
+**Fixture parameters are embedded directly** — `justfile:105-118`
+
+Quote-breaking or `$()` reaches the recipe shell before the CLI.
+
+```text
+catalog-generate-fixture OUTPUT GAME_ROOT GAME_ID:
+ cargo run ... --output "{{OUTPUT}}" --game-root "{{GAME_ROOT}}" --game-id "{{GAME_ID}}" --unrar "{{CATALOG_UNRAR}}"
+```
+
+**Production values repeat across a shell conditional** — `justfile:140-170`
+
+All cache/publisher invocations share the injection surface.
+
+```text
+catalog-generate-production PACKAGES_DIR:
+ #!/usr/bin/env bash
+ if ... --packages-dir "{{PACKAGES_DIR}}" ... --unrar "{{CATALOG_UNRAR}}"; then ... fi
+```
+
+#### Reachability
+
+Conditional on an operator or CI passing an attacker-influenced value.
+
+- **Attacker:** Package/path supplier or CI input actor.
+
+- **Entry point:** catalog/build/run just recipes.
+
+Preconditions:
+- Attacker-influenced just argument or LANSPREAD_UNRAR/GAMES_DIR
+- Operator executes recipe
+
+Existing controls:
+- Safe argv handling inside Rust publisher
+- Benign defaults
+
+**GAMES_DIR is embedded in shell text** — `justfile:22-31`
+
+Double quotes do not prevent shell command substitution after just renders the recipe.
+
+```text
+default:
+ #!/usr/bin/env bash
+ if [ -n "{{GAMES_DIR}}" ]; then
+ just run-production "{{GAMES_DIR}}"
+ else
+ just run-fixture
+ fi
+```
+
+**Fixture parameters are embedded directly** — `justfile:105-118`
+
+Quote-breaking or `$()` reaches the recipe shell before the CLI.
+
+```text
+catalog-generate-fixture OUTPUT GAME_ROOT GAME_ID:
+ cargo run ... --output "{{OUTPUT}}" --game-root "{{GAME_ROOT}}" --game-id "{{GAME_ID}}" --unrar "{{CATALOG_UNRAR}}"
+```
+
+**Production values repeat across a shell conditional** — `justfile:140-170`
+
+All cache/publisher invocations share the injection surface.
+
+```text
+catalog-generate-production PACKAGES_DIR:
+ #!/usr/bin/env bash
+ if ... --packages-dir "{{PACKAGES_DIR}}" ... --unrar "{{CATALOG_UNRAR}}"; then ... fi
+```
+
+#### Severity
+
+**Medium** — Successful exploitation executes with catalog/build operator authority, but requires attacker influence over an operator-supplied recipe argument or environment value.
+
+Severity increases in automated CI or workflows that pass externally supplied paths/IDs directly to just.
+
+Impact assessment:
+- **Level:** high
+- **Rationale:** Build secrets and trusted catalog/bundle inputs can be modified or exfiltrated.
+
+Likelihood assessment:
+- **Level:** medium
+- **Rationale:** Documented commands accept paths, but operators often choose them themselves.
+
+#### Remediation
+
+Pass data through environment/argv to non-shell helpers or apply just's correct shell-quoting facility to every argument and environment-derived value; test `$()`, quotes, whitespace, leading dashes, and newlines.
+
+
+
+### [8] Sybil peers can exhaust aggregate state and unbounded UI publication
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | The identity generation, first-come caps, per-author byte allowance, full projection cloning, and unbounded sender are explicit in source. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-peer/src/call_to_play.rs:555-565, crates/lanspread-peer/src/events.rs:21-25, crates/lanspread-peer/src/peer_db.rs:356-360, crates/lanspread-proto/src/lib.rs:15-20 |
+
+#### Summary
+
+One LAN host can generate enough valid peer identities to fill peer/author slots, retain hundreds of MiB of bounded-per-author state, and repeatedly enqueue cloned full views without an aggregate byte budget.
+
+#### Root Cause
+
+Identity-count limits are first-come and per-author bounds are not paired with aggregate retained-byte or queued-view budgets.
+
+**Per-author state is retained until a global author-count cap** — `crates/lanspread-peer/src/call_to_play.rs:555-565`
+
+The cap counts identities, not aggregate encoded bytes or physical origins.
+
+```rust
+if self.remote.len() >= MAX_CALL_TO_PLAY_AUTHORS - 1 {
+ return ObserveRemoteAuthorOutcome::AtCapacity;
+}
+self.remote.insert(author_id, RemoteAuthorSlice { ... });
+```
+
+**Every publication clones visible events across all authors** — `crates/lanspread-peer/src/call_to_play.rs:835-855`
+
+Repeated commits rebuild complete owned views.
+
+```rust
+for (author_id, slice) in &self.remote {
+ Self::extend_visible_author_events(*author_id, &slice.snapshot, now, &windows, &mut events);
+}
+events.sort_by_key(|event| (event.at, event.call_id, event.author_id, event.id));
+```
+
+**Complete views enter an unbounded channel** — `crates/lanspread-peer/src/events.rs:21-25`
+
+There is no capacity, backpressure, or replace-latest coalescing at this boundary.
+
+```rust
+pub fn send(tx_notify_ui: &UnboundedSender, event: PeerEvent) {
+ if let Err(err) = tx_notify_ui.send(event) { ... }
+}
+```
+
+**Large limits are per author/peer** — `crates/lanspread-proto/src/lib.rs:15-20`
+
+Sixty-three remote authors can retain about 252 MiB encoded before allocation overhead.
+
+```rust
+pub const MAX_LIBRARY_GAMES: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+#### Validation
+
+Validated and merged with the duplicate peer/author-slot candidate; exact content verification limits integrity impact but does not bound retained/queued state.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Per-author state is retained until a global author-count cap** — `crates/lanspread-peer/src/call_to_play.rs:555-565`
+
+The cap counts identities, not aggregate encoded bytes or physical origins.
+
+```rust
+if self.remote.len() >= MAX_CALL_TO_PLAY_AUTHORS - 1 {
+ return ObserveRemoteAuthorOutcome::AtCapacity;
+}
+self.remote.insert(author_id, RemoteAuthorSlice { ... });
+```
+
+**Every publication clones visible events across all authors** — `crates/lanspread-peer/src/call_to_play.rs:835-855`
+
+Repeated commits rebuild complete owned views.
+
+```rust
+for (author_id, slice) in &self.remote {
+ Self::extend_visible_author_events(*author_id, &slice.snapshot, now, &windows, &mut events);
+}
+events.sort_by_key(|event| (event.at, event.call_id, event.author_id, event.id));
+```
+
+**Complete views enter an unbounded channel** — `crates/lanspread-peer/src/events.rs:21-25`
+
+There is no capacity, backpressure, or replace-latest coalescing at this boundary.
+
+```rust
+pub fn send(tx_notify_ui: &UnboundedSender, event: PeerEvent) {
+ if let Err(err) = tx_notify_ui.send(event) { ... }
+}
+```
+
+**Large limits are per author/peer** — `crates/lanspread-proto/src/lib.rs:15-20`
+
+Sixty-three remote authors can retain about 252 MiB encoded before allocation overhead.
+
+```rust
+pub const MAX_LIBRARY_GAMES: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+Assertions:
+- One host can create many valid self-issued identities.
+- Per-author 4 MiB limits aggregate across 63 remote authors.
+- Full replacement views are owned clones sent through an unbounded channel.
+
+Counterevidence and remaining uncertainty:
+- Peer and author counts are finite.
+- TLS prevents impersonation of an existing PeerId.
+- Invalid snapshots and wrong content bytes are rejected.
+
+#### Dataflow
+
+Self-issued identities -\> pinned snapshots -\> retained per-peer/author maps -\> cloned aggregate projections -\> unbounded Tauri event queue.
+
+- **Source:** Attacker-operated mDNS/QUIC responders with valid keys.
+
+- **Sink:** Desktop process heap and peer-to-Tauri event queue.
+
+- **Outcome:** CPU/memory exhaustion and exclusion of legitimate new peers.
+
+**Per-author state is retained until a global author-count cap** — `crates/lanspread-peer/src/call_to_play.rs:555-565`
+
+The cap counts identities, not aggregate encoded bytes or physical origins.
+
+```rust
+if self.remote.len() >= MAX_CALL_TO_PLAY_AUTHORS - 1 {
+ return ObserveRemoteAuthorOutcome::AtCapacity;
+}
+self.remote.insert(author_id, RemoteAuthorSlice { ... });
+```
+
+**Every publication clones visible events across all authors** — `crates/lanspread-peer/src/call_to_play.rs:835-855`
+
+Repeated commits rebuild complete owned views.
+
+```rust
+for (author_id, slice) in &self.remote {
+ Self::extend_visible_author_events(*author_id, &slice.snapshot, now, &windows, &mut events);
+}
+events.sort_by_key(|event| (event.at, event.call_id, event.author_id, event.id));
+```
+
+**Complete views enter an unbounded channel** — `crates/lanspread-peer/src/events.rs:21-25`
+
+There is no capacity, backpressure, or replace-latest coalescing at this boundary.
+
+```rust
+pub fn send(tx_notify_ui: &UnboundedSender, event: PeerEvent) {
+ if let Err(err) = tx_notify_ui.send(event) { ... }
+}
+```
+
+**Large limits are per author/peer** — `crates/lanspread-proto/src/lib.rs:15-20`
+
+Sixty-three remote authors can retain about 252 MiB encoded before allocation overhead.
+
+```rust
+pub const MAX_LIBRARY_GAMES: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+#### Reachability
+
+Reachable from the same LAN while sharing/discovery is enabled; no victim key compromise is required.
+
+- **Attacker:** Malicious same-LAN host.
+
+- **Entry point:** mDNS candidate negotiation and Hello snapshot commit.
+
+Preconditions:
+- Sharing/discovery enabled
+- Dozens of distinct PeerIds and endpoints
+- Sustained valid snapshot traffic
+
+Existing controls:
+- 64-peer and 64-author count caps
+- 4 MiB per-author encoded cap
+- TLS responder pinning
+
+**Per-author state is retained until a global author-count cap** — `crates/lanspread-peer/src/call_to_play.rs:555-565`
+
+The cap counts identities, not aggregate encoded bytes or physical origins.
+
+```rust
+if self.remote.len() >= MAX_CALL_TO_PLAY_AUTHORS - 1 {
+ return ObserveRemoteAuthorOutcome::AtCapacity;
+}
+self.remote.insert(author_id, RemoteAuthorSlice { ... });
+```
+
+**Every publication clones visible events across all authors** — `crates/lanspread-peer/src/call_to_play.rs:835-855`
+
+Repeated commits rebuild complete owned views.
+
+```rust
+for (author_id, slice) in &self.remote {
+ Self::extend_visible_author_events(*author_id, &slice.snapshot, now, &windows, &mut events);
+}
+events.sort_by_key(|event| (event.at, event.call_id, event.author_id, event.id));
+```
+
+**Complete views enter an unbounded channel** — `crates/lanspread-peer/src/events.rs:21-25`
+
+There is no capacity, backpressure, or replace-latest coalescing at this boundary.
+
+```rust
+pub fn send(tx_notify_ui: &UnboundedSender, event: PeerEvent) {
+ if let Err(err) = tx_notify_ui.send(event) { ... }
+}
+```
+
+**Large limits are per author/peer** — `crates/lanspread-proto/src/lib.rs:15-20`
+
+Sixty-three remote authors can retain about 252 MiB encoded before allocation overhead.
+
+```rust
+pub const MAX_LIBRARY_GAMES: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_EVENTS_PER_AUTHOR: usize = 4_096;
+pub const MAX_CALL_TO_PLAY_AUTHOR_SNAPSHOT_BYTES: usize = 4 * 1024 * 1024;
+```
+
+#### Severity
+
+**Medium** — The impact can terminate the desktop process, but exploitation requires sustained LAN access, dozens of identities/endpoints, and substantial state transfer.
+
+Severity increases if fewer identities can cross the host memory limit or if the UI consumer is routinely slow.
+
+Impact assessment:
+- **Level:** high
+- **Rationale:** The application may freeze, swap heavily, or terminate and legitimate peers may be excluded.
+
+Likelihood assessment:
+- **Level:** medium
+- **Rationale:** The attack is practical but requires many identities and substantial traffic.
+
+#### Remediation
+
+Enforce aggregate byte/event budgets across remote peers, coalesce complete UI state through bounded latest-value channels, avoid full cloning per commit, and add fair/per-origin admission for identities.
+
+
+
+### [9] Unauthenticated hints can make the victim pull arbitrary known peers
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | The unbound payload identity, scheduler key, mismatch decision, and full-pull sink are explicit. |
+| Category | confused-deputy |
+| CWE | CWE-441 |
+| Affected lines | crates/lanspread-peer/src/services/stream.rs:265-271, crates/lanspread-peer/src/services/state_sync.rs:351-388, crates/lanspread-peer/src/services/remote_state.rs:147-159 |
+
+#### Summary
+
+Anonymous inbound connections may name any known PeerId in a change hint; a forged session/revision mismatch schedules a full pinned Hello pull to that unrelated peer.
+
+#### Root Cause
+
+A requester-anonymous protocol uses an untrusted claimed PeerId as a third-party work selector rather than binding or discarding hints.
+
+**Inbound connection identity is not checked** — `crates/lanspread-peer/src/services/stream.rs:265-271`
+
+The payload claim is accepted from any requester.
+
+```rust
+Request::LibraryChanged(hint) => {
+ ctx.state_sync.schedule_hint(StateDomain::Library, hint);
+ DispatchResult::close(framed_tx)
+}
+```
+
+**Claimed ID selects cached peer** — `crates/lanspread-peer/src/services/state_sync.rs:351-388`
+
+A random session forces a pull of the named known peer.
+
+```rust
+let snapshot = ctx.peer_liveness_for(peer_id).await;
+...
+perform_peer_refresh(ctx, snapshot).await
+...
+if snapshot.runtime_session_id != trigger.hint.runtime_session_id { return true; }
+```
+
+**Reconciliation performs Hello** — `crates/lanspread-peer/src/services/remote_state.rs:147-159`
+
+Authoritative state remains pinned, but resource work is delegated.
+
+```rust
+let snapshot = exchange_hello(&ctx.quic, &endpoint, &ctx.cancellation).await?;
+let prepared = PreparedSnapshot::prepare(endpoint.peer_id, generation, snapshot);
+commit_prepared(ctx, endpoint, ticket, prepared).await
+```
+
+#### Validation
+
+Validated. State integrity remains protected by the pinned pull, while resource consumption is the concrete impact.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Inbound connection identity is not checked** — `crates/lanspread-peer/src/services/stream.rs:265-271`
+
+The payload claim is accepted from any requester.
+
+```rust
+Request::LibraryChanged(hint) => {
+ ctx.state_sync.schedule_hint(StateDomain::Library, hint);
+ DispatchResult::close(framed_tx)
+}
+```
+
+**Claimed ID selects cached peer** — `crates/lanspread-peer/src/services/state_sync.rs:351-388`
+
+A random session forces a pull of the named known peer.
+
+```rust
+let snapshot = ctx.peer_liveness_for(peer_id).await;
+...
+perform_peer_refresh(ctx, snapshot).await
+...
+if snapshot.runtime_session_id != trigger.hint.runtime_session_id { return true; }
+```
+
+**Reconciliation performs Hello** — `crates/lanspread-peer/src/services/remote_state.rs:147-159`
+
+Authoritative state remains pinned, but resource work is delegated.
+
+```rust
+let snapshot = exchange_hello(&ctx.quic, &endpoint, &ctx.cancellation).await?;
+let prepared = PreparedSnapshot::prepare(endpoint.peer_id, generation, snapshot);
+commit_prepared(ctx, endpoint, ticket, prepared).await
+```
+
+Assertions:
+- Server has no client auth.
+- Claimed PeerId is the scheduler key.
+- Session mismatch forces a full pull.
+
+Counterevidence and remaining uncertainty:
+- Hint and pinned queues are bounded/separate.
+- At most eight pulls run concurrently.
+- Each peer is coalesced to one pull per five seconds.
+- Pulled state is TLS pinned.
+
+#### Dataflow
+
+Inbound hint -\> claimed PeerId -\> state-sync slot -\> cached pinned endpoint -\> full Hello exchange.
+
+- **Source:** Unauthenticated ChangeHint fields.
+
+- **Sink:** Victim connector and unrelated peer's Hello responder.
+
+- **Outcome:** Bandwidth and CPU amplification/availability degradation.
+
+**Inbound connection identity is not checked** — `crates/lanspread-peer/src/services/stream.rs:265-271`
+
+The payload claim is accepted from any requester.
+
+```rust
+Request::LibraryChanged(hint) => {
+ ctx.state_sync.schedule_hint(StateDomain::Library, hint);
+ DispatchResult::close(framed_tx)
+}
+```
+
+**Claimed ID selects cached peer** — `crates/lanspread-peer/src/services/state_sync.rs:351-388`
+
+A random session forces a pull of the named known peer.
+
+```rust
+let snapshot = ctx.peer_liveness_for(peer_id).await;
+...
+perform_peer_refresh(ctx, snapshot).await
+...
+if snapshot.runtime_session_id != trigger.hint.runtime_session_id { return true; }
+```
+
+**Reconciliation performs Hello** — `crates/lanspread-peer/src/services/remote_state.rs:147-159`
+
+Authoritative state remains pinned, but resource work is delegated.
+
+```rust
+let snapshot = exchange_hello(&ctx.quic, &endpoint, &ctx.cancellation).await?;
+let prepared = PreparedSnapshot::prepare(endpoint.peer_id, generation, snapshot);
+commit_prepared(ctx, endpoint, ticket, prepared).await
+```
+
+#### Reachability
+
+Direct while sharing is enabled and at least one peer is known.
+
+- **Attacker:** Any reachable LAN host.
+
+- **Entry point:** LibraryChanged or CallToPlayChanged.
+
+Preconditions:
+- Sharing enabled
+- Victim has known peers
+- Repeated forged session/revision values
+
+Existing controls:
+- 64-slot hint queue
+- 8 concurrent pulls
+- 5-second per-peer coalescing
+- Pinned TLS follow-up
+
+**Inbound connection identity is not checked** — `crates/lanspread-peer/src/services/stream.rs:265-271`
+
+The payload claim is accepted from any requester.
+
+```rust
+Request::LibraryChanged(hint) => {
+ ctx.state_sync.schedule_hint(StateDomain::Library, hint);
+ DispatchResult::close(framed_tx)
+}
+```
+
+**Claimed ID selects cached peer** — `crates/lanspread-peer/src/services/state_sync.rs:351-388`
+
+A random session forces a pull of the named known peer.
+
+```rust
+let snapshot = ctx.peer_liveness_for(peer_id).await;
+...
+perform_peer_refresh(ctx, snapshot).await
+...
+if snapshot.runtime_session_id != trigger.hint.runtime_session_id { return true; }
+```
+
+**Reconciliation performs Hello** — `crates/lanspread-peer/src/services/remote_state.rs:147-159`
+
+Authoritative state remains pinned, but resource work is delegated.
+
+```rust
+let snapshot = exchange_hello(&ctx.quic, &endpoint, &ctx.cancellation).await?;
+let prepared = PreparedSnapshot::prepare(endpoint.peer_id, generation, snapshot);
+commit_prepared(ctx, endpoint, ticket, prepared).await
+```
+
+#### Severity
+
+**Medium** — Small anonymous requests can repeatedly consume victim and third-party bandwidth/serialization CPU, but concurrency, tracked peers, and per-peer cadence are bounded.
+
+Severity increases with large valid snapshots or metered/slow links.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** Can consume significant bandwidth/serialization work but cannot inject authoritative state.
+
+Likelihood assessment:
+- **Level:** high
+- **Rationale:** No credentials or target private keys are required.
+
+#### Remediation
+
+If requester authentication remains absent, drop remote change hints and rely on pinned liveness reconciliation. Otherwise bind the hint to an authenticated client PeerId and remove the payload identity.
+
+
+
+### [10] Mutable game scripts are launched elevated without launch-time trust binding
+
+| Field | Value |
+| --- | --- |
+| Severity | medium |
+| Confidence | high |
+| Confidence rationale | The path checks, missing catalog/digest checks, and runas calls are explicit. |
+| Category | privilege-escalation |
+| CWE | CWE-250, CWE-73 |
+| Affected lines | crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1866-1958, crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2007-2064, crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1554-1582 |
+
+#### Summary
+
+Windows play/server commands follow mutable paths under any selected game root and pass setup/start scripts to `cmd.exe` with `runas` without rechecking catalog membership, digest, installed ownership, or reparse-free containment.
+
+#### Root Cause
+
+The Windows launch path is separate from the catalog-bound no-follow install/download capabilities and treats filename/existence plus user action as sufficient authority for elevation.
+
+**Game scripts are path-checked then elevated** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1891-1951`
+
+No manifest, digest, retained handle, or reparse check binds the launched script.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let game_setup_bin = game_path.join(GAME_SETUP_SCRIPT);
+let game_start_bin = game_path.join(GAME_START_SCRIPT);
+...
+run_as_admin_and_wait("cmd.exe", &setup_params, &game_dir, ...)?;
+...
+run_as_admin_detached("cmd.exe", &script_params(&game_start_bin, ...), ...);
+```
+
+**Server script has the same gap** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2026-2057`
+
+Path-based `is_file` follows redirections and does not prove catalog authority.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let server_start_bin = game_path.join(SERVER_START_SCRIPT);
+if !server_start_bin.is_file() { return Ok(false); }
+...
+run_as_admin_detached("cmd.exe", &server_script_params(&server_start_bin, ...), ...);
+```
+
+**Detached launcher requests elevation** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1563-1577`
+
+The explicit UAC boundary makes mutable-script provenance security-sensitive.
+
+```rust
+let runas_wide = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
+ShellExecuteW(None, PCWSTR::from_raw(runas_wide.as_ptr()), ...);
+```
+
+#### Validation
+
+Validated with severity reduced from high for Windows/UAC/local-write prerequisites. Remote peers alone cannot substitute bytes because downloads remain catalog-verified.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Game scripts are path-checked then elevated** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1891-1951`
+
+No manifest, digest, retained handle, or reparse check binds the launched script.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let game_setup_bin = game_path.join(GAME_SETUP_SCRIPT);
+let game_start_bin = game_path.join(GAME_START_SCRIPT);
+...
+run_as_admin_and_wait("cmd.exe", &setup_params, &game_dir, ...)?;
+...
+run_as_admin_detached("cmd.exe", &script_params(&game_start_bin, ...), ...);
+```
+
+**Server script has the same gap** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2026-2057`
+
+Path-based `is_file` follows redirections and does not prove catalog authority.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let server_start_bin = game_path.join(SERVER_START_SCRIPT);
+if !server_start_bin.is_file() { return Ok(false); }
+...
+run_as_admin_detached("cmd.exe", &server_script_params(&server_start_bin, ...), ...);
+```
+
+**Detached launcher requests elevation** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1563-1577`
+
+The explicit UAC boundary makes mutable-script provenance security-sensitive.
+
+```rust
+let runas_wide = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
+ShellExecuteW(None, PCWSTR::from_raw(runas_wide.as_ptr()), ...);
+```
+
+Assertions:
+- Any existing selected directory can become the games root.
+- Sink functions accept single-component IDs without a catalog lookup.
+- Scripts are elevated by path after only existence/local checks.
+
+Counterevidence and remaining uncertainty:
+- IDs are one component and user settings are sanitized.
+- Downloaded/streamed bytes are catalog verified.
+- The victim must invoke play/server and approve UAC.
+
+#### Dataflow
+
+Selected directory + game ID -\> joined mutable script path -\> string cmd.exe parameters -\> ShellExecute runas.
+
+- **Source:** Locally/shared-tree controlled game scripts.
+
+- **Sink:** Elevated Windows command processor.
+
+- **Outcome:** Administrator execution of attacker-controlled batch content.
+
+**Game scripts are path-checked then elevated** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1891-1951`
+
+No manifest, digest, retained handle, or reparse check binds the launched script.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let game_setup_bin = game_path.join(GAME_SETUP_SCRIPT);
+let game_start_bin = game_path.join(GAME_START_SCRIPT);
+...
+run_as_admin_and_wait("cmd.exe", &setup_params, &game_dir, ...)?;
+...
+run_as_admin_detached("cmd.exe", &script_params(&game_start_bin, ...), ...);
+```
+
+**Server script has the same gap** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2026-2057`
+
+Path-based `is_file` follows redirections and does not prove catalog authority.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let server_start_bin = game_path.join(SERVER_START_SCRIPT);
+if !server_start_bin.is_file() { return Ok(false); }
+...
+run_as_admin_detached("cmd.exe", &server_script_params(&server_start_bin, ...), ...);
+```
+
+**Detached launcher requests elevation** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1563-1577`
+
+The explicit UAC boundary makes mutable-script provenance security-sensitive.
+
+```rust
+let runas_wide = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
+ShellExecuteW(None, PCWSTR::from_raw(runas_wide.as_ptr()), ...);
+```
+
+#### Reachability
+
+Conditional on Windows, selected-root write influence, local launch, and UAC approval.
+
+- **Attacker:** Lower-privileged local or shared-directory supplier.
+
+- **Entry point:** run_game or start_server Tauri command.
+
+Preconditions:
+- Windows
+- Writable or crafted selected game root
+- Victim launch action
+- UAC approval
+
+Existing controls:
+- Single-component game IDs
+- Catalog integrity for peer downloads
+- UAC prompt
+- Installed/local presence checks
+
+**Game scripts are path-checked then elevated** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1891-1951`
+
+No manifest, digest, retained handle, or reparse check binds the launched script.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let game_setup_bin = game_path.join(GAME_SETUP_SCRIPT);
+let game_start_bin = game_path.join(GAME_START_SCRIPT);
+...
+run_as_admin_and_wait("cmd.exe", &setup_params, &game_dir, ...)?;
+...
+run_as_admin_detached("cmd.exe", &script_params(&game_start_bin, ...), ...);
+```
+
+**Server script has the same gap** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:2026-2057`
+
+Path-based `is_file` follows redirections and does not prove catalog authority.
+
+```rust
+let game_path = games_folder.join(id.clone());
+let server_start_bin = game_path.join(SERVER_START_SCRIPT);
+if !server_start_bin.is_file() { return Ok(false); }
+...
+run_as_admin_detached("cmd.exe", &server_script_params(&server_start_bin, ...), ...);
+```
+
+**Detached launcher requests elevation** — `crates/lanspread-tauri-deno-ts/src-tauri/src/lib.rs:1563-1577`
+
+The explicit UAC boundary makes mutable-script provenance security-sensitive.
+
+```rust
+let runas_wide = OsStr::new("runas").encode_wide().chain(Some(0)).collect();
+ShellExecuteW(None, PCWSTR::from_raw(runas_wide.as_ptr()), ...);
+```
+
+#### Severity
+
+**Medium** — The consequence is administrator code execution, but exploitation requires local/shared-tree write influence, a victim launch action, Windows, and UAC approval.
+
+Severity increases where selected game directories are shared across trust boundaries or UAC prompts are routinely approved.
+
+Impact assessment:
+- **Level:** high
+- **Rationale:** Successful exploitation executes attacker code with the elevated token.
+
+Likelihood assessment:
+- **Level:** medium
+- **Rationale:** Multiple local/user-interaction prerequisites materially constrain exploitation.
+
+#### Remediation
+
+Run ordinary game/server scripts as the current user. For setup requiring elevation, require a catalog game and authoritative installed state, verify the exact script digest immediately before launch through no-follow handles, reject reparse points, and preserve the verified object identity into process creation.
+
+
+
+### [11] Selected game directories can trigger unbounded monitoring and migration work
+
+| Field | Value |
+| --- | --- |
+| Severity | low |
+| Confidence | high |
+| Confidence rationale | Unbounded enumeration, task spawn, WalkDir traversal, and whole-file reads are explicit. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-peer/src/services/local_monitor.rs:200-235, crates/lanspread-peer/src/services/local_monitor.rs:329-376, crates/lanspread-peer/src/local_games.rs:268-294, crates/lanspread-peer/src/migration.rs:284-297 |
+
+#### Summary
+
+The continuously monitored game root retains every top-level name, spawns one task per changed ID, recursively walks trees, and reads current/legacy state files without byte budgets.
+
+#### Root Cause
+
+Local filesystem inputs have shape/link checks but no aggregate cardinality, traversal, task, or persisted-file byte budgets.
+
+**Every root name enters the snapshot** — `crates/lanspread-peer/src/services/local_monitor.rs:200-235`
+
+No catalog intersection or entry cap precedes retention.
+
+```rust
+let mut games = BTreeMap::new();
+for entry in fs::read_dir(game_dir) {
+ ...
+ games.insert(id.to_owned(), game_root);
+}
+```
+
+**Every changed ID spawns a task** — `crates/lanspread-peer/src/services/local_monitor.rs:329-376`
+
+Only duplicate work for the same ID coalesces; no global worker cap applies.
+
+```rust
+for id in ready_ids {
+ queue_rescan(ctx, tx_notify_ui, gate, rescans, id).await;
+}
+...
+rescans.spawn(async move { run_gated_rescan(...).await; });
+```
+
+**Current index is read without a byte cap** — `crates/lanspread-peer/src/local_games.rs:268-284`
+
+A tampered app-state file can allocate arbitrarily.
+
+```rust
+let data = match std::fs::read_to_string(path) { ... };
+match serde_json::from_str(&data) { ... }
+```
+
+**Legacy migration duplicates an unbounded file** — `crates/lanspread-peer/src/migration.rs:284-297`
+
+Selection automatically crosses this legacy input boundary.
+
+```rust
+let data = fs::read(legacy_path)?;
+write_bytes_atomically(target_path, &data)?;
+remove_file_if_exists(legacy_path)?;
+```
+
+#### Validation
+
+Validated and merged with the legacy-index candidate. Severity is low because meaningful influence is local/shared-root rather than ordinary LAN peer input.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Every root name enters the snapshot** — `crates/lanspread-peer/src/services/local_monitor.rs:200-235`
+
+No catalog intersection or entry cap precedes retention.
+
+```rust
+let mut games = BTreeMap::new();
+for entry in fs::read_dir(game_dir) {
+ ...
+ games.insert(id.to_owned(), game_root);
+}
+```
+
+**Every changed ID spawns a task** — `crates/lanspread-peer/src/services/local_monitor.rs:329-376`
+
+Only duplicate work for the same ID coalesces; no global worker cap applies.
+
+```rust
+for id in ready_ids {
+ queue_rescan(ctx, tx_notify_ui, gate, rescans, id).await;
+}
+...
+rescans.spawn(async move { run_gated_rescan(...).await; });
+```
+
+**Current index is read without a byte cap** — `crates/lanspread-peer/src/local_games.rs:268-284`
+
+A tampered app-state file can allocate arbitrarily.
+
+```rust
+let data = match std::fs::read_to_string(path) { ... };
+match serde_json::from_str(&data) { ... }
+```
+
+**Legacy migration duplicates an unbounded file** — `crates/lanspread-peer/src/migration.rs:284-297`
+
+Selection automatically crosses this legacy input boundary.
+
+```rust
+let data = fs::read(legacy_path)?;
+write_bytes_atomically(target_path, &data)?;
+remove_file_if_exists(legacy_path)?;
+```
+
+Assertions:
+- Polling occurs every second.
+- All root names are retained before catalog filtering.
+- Per-ID task fan-out is not globally bounded.
+- Index/migration reads allocate complete files.
+
+Counterevidence and remaining uncertainty:
+- Symlinks/reparse points and special files are rejected.
+- Repeated work for one ID coalesces.
+- Remote catalog downloads cannot create arbitrary top-level entries.
+
+#### Dataflow
+
+Selected filesystem -\> root snapshot/WalkDir/whole-file read -\> task set/heap/logs.
+
+- **Source:** Locally or share-writable selected game directory and persisted index files.
+
+- **Sink:** Desktop heap, blocking pool, async task set, and log file.
+
+- **Outcome:** Persistent CPU/memory exhaustion or startup OOM.
+
+**Every root name enters the snapshot** — `crates/lanspread-peer/src/services/local_monitor.rs:200-235`
+
+No catalog intersection or entry cap precedes retention.
+
+```rust
+let mut games = BTreeMap::new();
+for entry in fs::read_dir(game_dir) {
+ ...
+ games.insert(id.to_owned(), game_root);
+}
+```
+
+**Every changed ID spawns a task** — `crates/lanspread-peer/src/services/local_monitor.rs:329-376`
+
+Only duplicate work for the same ID coalesces; no global worker cap applies.
+
+```rust
+for id in ready_ids {
+ queue_rescan(ctx, tx_notify_ui, gate, rescans, id).await;
+}
+...
+rescans.spawn(async move { run_gated_rescan(...).await; });
+```
+
+**Current index is read without a byte cap** — `crates/lanspread-peer/src/local_games.rs:268-284`
+
+A tampered app-state file can allocate arbitrarily.
+
+```rust
+let data = match std::fs::read_to_string(path) { ... };
+match serde_json::from_str(&data) { ... }
+```
+
+**Legacy migration duplicates an unbounded file** — `crates/lanspread-peer/src/migration.rs:284-297`
+
+Selection automatically crosses this legacy input boundary.
+
+```rust
+let data = fs::read(legacy_path)?;
+write_bytes_atomically(target_path, &data)?;
+remove_file_if_exists(legacy_path)?;
+```
+
+#### Reachability
+
+Requires the operator to select or restore an attacker-influenced directory.
+
+- **Attacker:** Lower-privileged local/shared-filesystem actor.
+
+- **Entry point:** Game-directory activation and one-second local monitor.
+
+Preconditions:
+- Attacker-controlled directory contents
+- Victim selects/restores directory
+
+Existing controls:
+- Link/reparse rejection
+- Special-file filtering
+- Per-ID rescan coalescing
+- Missed-tick skipping
+
+**Every root name enters the snapshot** — `crates/lanspread-peer/src/services/local_monitor.rs:200-235`
+
+No catalog intersection or entry cap precedes retention.
+
+```rust
+let mut games = BTreeMap::new();
+for entry in fs::read_dir(game_dir) {
+ ...
+ games.insert(id.to_owned(), game_root);
+}
+```
+
+**Every changed ID spawns a task** — `crates/lanspread-peer/src/services/local_monitor.rs:329-376`
+
+Only duplicate work for the same ID coalesces; no global worker cap applies.
+
+```rust
+for id in ready_ids {
+ queue_rescan(ctx, tx_notify_ui, gate, rescans, id).await;
+}
+...
+rescans.spawn(async move { run_gated_rescan(...).await; });
+```
+
+**Current index is read without a byte cap** — `crates/lanspread-peer/src/local_games.rs:268-284`
+
+A tampered app-state file can allocate arbitrarily.
+
+```rust
+let data = match std::fs::read_to_string(path) { ... };
+match serde_json::from_str(&data) { ... }
+```
+
+**Legacy migration duplicates an unbounded file** — `crates/lanspread-peer/src/migration.rs:284-297`
+
+Selection automatically crosses this legacy input boundary.
+
+```rust
+let data = fs::read(legacy_path)?;
+write_bytes_atomically(target_path, &data)?;
+remove_file_if_exists(legacy_path)?;
+```
+
+#### Severity
+
+**Low** — The desktop can be exhausted, but the attacker needs local/shared/removable-directory control plus operator selection; remote peers cannot create this fan-out.
+
+Severity increases when selected game roots are writable by lower-privileged or remote filesystem users.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** Can stall or terminate the peer but does not grant file escape or code execution.
+
+Likelihood assessment:
+- **Level:** low
+- **Rationale:** Requires local/shared filesystem placement and user selection.
+
+#### Remediation
+
+Intersect names with the catalog before retention/task admission; add root/per-game/depth/metadata budgets and a bounded rescan pool; bounded-read current and legacy indexes/version.ini and avoid raw invalid-content logging.
+
+
+
+### [12] Public state helpers permit marker writes outside the state directory
+
+| Field | Value |
+| --- | --- |
+| Severity | low |
+| Confidence | high |
+| Confidence rationale | The raw joins, public re-exports, and filesystem write are direct. |
+| Category | path-traversal |
+| CWE | CWE-22 |
+| Affected lines | crates/lanspread-peer/src/state_paths.rs:44-59, crates/lanspread-peer/src/launch_settings.rs:168-175, crates/lanspread-peer/src/launch_settings.rs:383-389 |
+
+#### Summary
+
+Public per-game path and marker APIs join an unvalidated game ID; absolute or parent-containing IDs escape `state_dir/games`, after which the marker writer creates parents and overwrites a fixed file.
+
+#### Root Cause
+
+The public API represents game IDs as raw strings instead of a validated single-component type.
+
+**Raw game ID selects the state child** — `crates/lanspread-peer/src/state_paths.rs:44-59`
+
+Absolute and parent components are not rejected before Path::join.
+
+```rust
+pub(crate) fn game_state_dir(state_dir: &Path, game_id: &str) -> PathBuf {
+ games_state_dir(state_dir).join(game_id)
+}
+...
+pub fn launch_settings_applied_path(state_dir: &Path, game_id: &str) -> PathBuf {
+ game_state_dir(state_dir, game_id).join(LAUNCH_SETTINGS_APPLIED_FILE)
+}
+```
+
+**Public API writes the escaped marker** — `crates/lanspread-peer/src/launch_settings.rs:168-175`
+
+The public function documents no validated-ID precondition.
+
+```rust
+pub fn mark_launch_settings_applied(state_dir: &Path, game_id: &str) -> eyre::Result<()> {
+ scoped_blocking(|| mark_applied(&launch_settings_applied_path(state_dir, game_id)))
+}
+```
+
+**Parents are created and marker overwritten** — `crates/lanspread-peer/src/launch_settings.rs:383-389`
+
+The escaped path drives mutation with application privileges.
+
+```rust
+if let Some(parent) = marker.parent() {
+ std::fs::create_dir_all(parent)?;
+}
+std::fs::write(marker, [])?;
+```
+
+#### Validation
+
+Validated with low severity; no direct LAN route exists and current shipping callers use validated IDs.
+
+Validation method: Static public-API trace from caller-controlled identifier through Path::join to filesystem write.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Raw game ID selects the state child** — `crates/lanspread-peer/src/state_paths.rs:44-59`
+
+Absolute and parent components are not rejected before Path::join.
+
+```rust
+pub(crate) fn game_state_dir(state_dir: &Path, game_id: &str) -> PathBuf {
+ games_state_dir(state_dir).join(game_id)
+}
+...
+pub fn launch_settings_applied_path(state_dir: &Path, game_id: &str) -> PathBuf {
+ game_state_dir(state_dir, game_id).join(LAUNCH_SETTINGS_APPLIED_FILE)
+}
+```
+
+**Public API writes the escaped marker** — `crates/lanspread-peer/src/launch_settings.rs:168-175`
+
+The public function documents no validated-ID precondition.
+
+```rust
+pub fn mark_launch_settings_applied(state_dir: &Path, game_id: &str) -> eyre::Result<()> {
+ scoped_blocking(|| mark_applied(&launch_settings_applied_path(state_dir, game_id)))
+}
+```
+
+**Parents are created and marker overwritten** — `crates/lanspread-peer/src/launch_settings.rs:383-389`
+
+The escaped path drives mutation with application privileges.
+
+```rust
+if let Some(parent) = marker.parent() {
+ std::fs::create_dir_all(parent)?;
+}
+std::fs::write(marker, [])?;
+```
+
+Assertions:
+- Absolute child replaces the accumulated PathBuf prefix.
+- Parent components escape when resolved.
+- Public marker API creates parents and writes.
+
+Counterevidence and remaining uncertainty:
+- Windows run-game checks one component.
+- Internal StreamInstall calls use catalog IDs.
+- Only a fixed basename is written.
+
+#### Dataflow
+
+Raw game_id -\> public path helper -\> escaped PathBuf -\> create_dir_all/write.
+
+- **Source:** Embedding application input.
+
+- **Sink:** Filesystem outside state_dir.
+
+- **Outcome:** Fixed marker file creation/overwrite.
+
+**Raw game ID selects the state child** — `crates/lanspread-peer/src/state_paths.rs:44-59`
+
+Absolute and parent components are not rejected before Path::join.
+
+```rust
+pub(crate) fn game_state_dir(state_dir: &Path, game_id: &str) -> PathBuf {
+ games_state_dir(state_dir).join(game_id)
+}
+...
+pub fn launch_settings_applied_path(state_dir: &Path, game_id: &str) -> PathBuf {
+ game_state_dir(state_dir, game_id).join(LAUNCH_SETTINGS_APPLIED_FILE)
+}
+```
+
+**Public API writes the escaped marker** — `crates/lanspread-peer/src/launch_settings.rs:168-175`
+
+The public function documents no validated-ID precondition.
+
+```rust
+pub fn mark_launch_settings_applied(state_dir: &Path, game_id: &str) -> eyre::Result<()> {
+ scoped_blocking(|| mark_applied(&launch_settings_applied_path(state_dir, game_id)))
+}
+```
+
+**Parents are created and marker overwritten** — `crates/lanspread-peer/src/launch_settings.rs:383-389`
+
+The escaped path drives mutation with application privileges.
+
+```rust
+if let Some(parent) = marker.parent() {
+ std::fs::create_dir_all(parent)?;
+}
+std::fs::write(marker, [])?;
+```
+
+#### Reachability
+
+Public Rust API boundary; no shipping LAN route was found.
+
+- **Attacker:** Caller controlling a game_id consumed by an embedding application.
+
+- **Entry point:** mark_launch_settings_applied or exported path helpers.
+
+Preconditions:
+- Embedding caller forwards untrusted identifier
+
+Existing controls:
+- Current internal catalog/single-component checks
+- Fixed marker basename
+
+**Public API writes the escaped marker** — `crates/lanspread-peer/src/launch_settings.rs:168-175`
+
+The public function documents no validated-ID precondition.
+
+```rust
+pub fn mark_launch_settings_applied(state_dir: &Path, game_id: &str) -> eyre::Result<()> {
+ scoped_blocking(|| mark_applied(&launch_settings_applied_path(state_dir, game_id)))
+}
+```
+
+#### Severity
+
+**Low** — The write is constrained to a fixed basename and shipping callers validate/catalog-source IDs, but an embedding caller forwarding untrusted IDs crosses a real public library boundary.
+
+Additional runtime or deployment evidence could raise or lower this severity.
+
+Impact assessment:
+- **Level:** low
+- **Rationale:** Write target basename is fixed, but directories/path can escape.
+
+Likelihood assessment:
+- **Level:** low
+- **Rationale:** Current workspace callers prevalidate; exploitation depends on another caller.
+
+#### Remediation
+
+Require a validated single-component GameId newtype or make helpers fallible and reject absolute/prefix/dot/parent/separator/reserved forms; write relative to a retained no-follow state-directory handle.
+
+
+
+### [13] Catalog preflight can read outside the package root through links and Windows races
+
+| Field | Value |
+| --- | --- |
+| Severity | low |
+| Confidence | high |
+| Confidence rationale | Ordering, whole diagnostic, and non-Unix `same_file=true` are explicit. |
+| Category | path-traversal |
+| CWE | CWE-59, CWE-367 |
+| Affected lines | crates/lanspread-compat/src/catalog_publisher/mod.rs:120-130, crates/lanspread-compat/src/catalog_publisher/package.rs:125-137, crates/lanspread-compat/src/catalog_publisher/package.rs:403-425 |
+
+#### Summary
+
+Version preflight joins `package_root/version.ini` before validating the game root, and Windows disables file-identity comparison, allowing ancestor-link traversal or a raced reparse target to be read and echoed.
+
+#### Root Cause
+
+Package reads are path-based rather than rooted in retained no-follow directory handles, and cross-platform object identity is not enforced.
+
+**Version read precedes root validation** — `crates/lanspread-compat/src/catalog_publisher/mod.rs:120-130`
+
+The later manifest builder's non-link directory validation has not run.
+
+```rust
+for game in &selected {
+ package::validate_package_version(
+ &options.packages_dir.join(&game.game_id),
+ &game.game_version,
+ )?;
+}
+```
+
+**Out-of-root bytes are echoed on mismatch** — `crates/lanspread-compat/src/catalog_publisher/package.rs:125-137`
+
+Ancestor links are followed by the path open; mismatching content reaches stderr.
+
+```rust
+let path = package_root.join("version.ini");
+let bytes = read_bounded_regular_file(&path, MAX_VERSION_INI_BYTES)?;
+let version = std::str::from_utf8(&bytes)?.trim();
+if version != expected {
+ eyre::bail!("... version.ini contains {version:?}");
+}
+```
+
+**Windows accepts every check/open identity** — `crates/lanspread-compat/src/catalog_publisher/package.rs:403-425`
+
+The pre-open reparse check is not bound to the opened handle on Windows.
+
+```rust
+#[cfg(not(unix))]
+fn same_file(_before: &fs::Metadata, _after: &fs::Metadata) -> bool {
+ true
+}
+...
+#[cfg(windows)]
+fn is_link_or_reparse(metadata: &fs::Metadata) -> bool { ... }
+```
+
+#### Validation
+
+Validated and merged with the Windows TOCTOU candidate. Publication fail-closed behavior limits the impact to bounded disclosure/digest-oracle and provenance violation.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Version read precedes root validation** — `crates/lanspread-compat/src/catalog_publisher/mod.rs:120-130`
+
+The later manifest builder's non-link directory validation has not run.
+
+```rust
+for game in &selected {
+ package::validate_package_version(
+ &options.packages_dir.join(&game.game_id),
+ &game.game_version,
+ )?;
+}
+```
+
+**Out-of-root bytes are echoed on mismatch** — `crates/lanspread-compat/src/catalog_publisher/package.rs:125-137`
+
+Ancestor links are followed by the path open; mismatching content reaches stderr.
+
+```rust
+let path = package_root.join("version.ini");
+let bytes = read_bounded_regular_file(&path, MAX_VERSION_INI_BYTES)?;
+let version = std::str::from_utf8(&bytes)?.trim();
+if version != expected {
+ eyre::bail!("... version.ini contains {version:?}");
+}
+```
+
+**Windows accepts every check/open identity** — `crates/lanspread-compat/src/catalog_publisher/package.rs:403-425`
+
+The pre-open reparse check is not bound to the opened handle on Windows.
+
+```rust
+#[cfg(not(unix))]
+fn same_file(_before: &fs::Metadata, _after: &fs::Metadata) -> bool {
+ true
+}
+...
+#[cfg(windows)]
+fn is_link_or_reparse(metadata: &fs::Metadata) -> bool { ... }
+```
+
+Assertions:
+- Ancestor symlink validation is absent before preflight.
+- Final-component checks do not prove ancestor confinement.
+- Windows same_file unconditionally succeeds.
+- Mismatch errors include contents.
+
+Counterevidence and remaining uncertainty:
+- Stable final-component links/reparse points are rejected.
+- Read is capped at 64 KiB.
+- Later manifest building rejects a stable linked root.
+- Unix final-component swaps are device/inode checked.
+
+#### Dataflow
+
+packages_dir/game_id path -\> joined version.ini -\> path-based lstat/open -\> UTF-8 contents -\> error log.
+
+- **Source:** Attacker-controlled package tree links/races.
+
+- **Sink:** Publisher memory and stderr/CI log.
+
+- **Outcome:** Bounded out-of-root disclosure; Windows hash path may expose a digest oracle.
+
+**Version read precedes root validation** — `crates/lanspread-compat/src/catalog_publisher/mod.rs:120-130`
+
+The later manifest builder's non-link directory validation has not run.
+
+```rust
+for game in &selected {
+ package::validate_package_version(
+ &options.packages_dir.join(&game.game_id),
+ &game.game_version,
+ )?;
+}
+```
+
+**Out-of-root bytes are echoed on mismatch** — `crates/lanspread-compat/src/catalog_publisher/package.rs:125-137`
+
+Ancestor links are followed by the path open; mismatching content reaches stderr.
+
+```rust
+let path = package_root.join("version.ini");
+let bytes = read_bounded_regular_file(&path, MAX_VERSION_INI_BYTES)?;
+let version = std::str::from_utf8(&bytes)?.trim();
+if version != expected {
+ eyre::bail!("... version.ini contains {version:?}");
+}
+```
+
+**Windows accepts every check/open identity** — `crates/lanspread-compat/src/catalog_publisher/package.rs:403-425`
+
+The pre-open reparse check is not bound to the opened handle on Windows.
+
+```rust
+#[cfg(not(unix))]
+fn same_file(_before: &fs::Metadata, _after: &fs::Metadata) -> bool {
+ true
+}
+...
+#[cfg(windows)]
+fn is_link_or_reparse(metadata: &fs::Metadata) -> bool { ... }
+```
+
+#### Reachability
+
+Requires publisher execution on an attacker-controlled package tree; Windows race is platform-specific.
+
+- **Attacker:** Package-corpus supplier or concurrent local process.
+
+- **Entry point:** Catalog generation version preflight.
+
+Preconditions:
+- Readable target named version.ini or race target
+- Access to resulting diagnostics for disclosure
+
+Existing controls:
+- 64 KiB version limit
+- Final-component link/reparse check
+- Unix inode/device comparison
+- Later root validation
+
+**Version read precedes root validation** — `crates/lanspread-compat/src/catalog_publisher/mod.rs:120-130`
+
+The later manifest builder's non-link directory validation has not run.
+
+```rust
+for game in &selected {
+ package::validate_package_version(
+ &options.packages_dir.join(&game.game_id),
+ &game.game_version,
+ )?;
+}
+```
+
+**Out-of-root bytes are echoed on mismatch** — `crates/lanspread-compat/src/catalog_publisher/package.rs:125-137`
+
+Ancestor links are followed by the path open; mismatching content reaches stderr.
+
+```rust
+let path = package_root.join("version.ini");
+let bytes = read_bounded_regular_file(&path, MAX_VERSION_INI_BYTES)?;
+let version = std::str::from_utf8(&bytes)?.trim();
+if version != expected {
+ eyre::bail!("... version.ini contains {version:?}");
+}
+```
+
+**Windows accepts every check/open identity** — `crates/lanspread-compat/src/catalog_publisher/package.rs:403-425`
+
+The pre-open reparse check is not bound to the opened handle on Windows.
+
+```rust
+#[cfg(not(unix))]
+fn same_file(_before: &fs::Metadata, _after: &fs::Metadata) -> bool {
+ true
+}
+...
+#[cfg(windows)]
+fn is_link_or_reparse(metadata: &fs::Metadata) -> bool { ... }
+```
+
+#### Severity
+
+**Low** — The read is bounded and publication later fails; disclosure requires package-tree control and access to operator/CI diagnostics, with Windows race prerequisites for the TOCTOU variant.
+
+Severity increases when publisher logs are visible to package suppliers or the build account has sensitive readable version.ini files.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** Can expose bounded sensitive content but cannot publish a stable linked package.
+
+Likelihood assessment:
+- **Level:** low
+- **Rationale:** Requires specially placed links or a filesystem race and often log visibility.
+
+#### Remediation
+
+Open the packages/game roots through retained no-follow handles and open `version.ini` relative to them; implement Windows file-ID/reparse-safe identity checks; never print raw untrusted version contents.
+
+
+
+### [14] Cross-author event-ID collisions can suppress Call-to-Play entries
+
+| Field | Value |
+| --- | --- |
+| Severity | low |
+| Confidence | high |
+| Confidence rationale | Producer preserves author_id while the reducer's global Map key omits it. |
+| Category | state-integrity |
+| CWE | CWE-694 |
+| Affected lines | crates/lanspread-peer/src/call_to_play.rs:961-979, crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts:135-145 |
+
+#### Summary
+
+Rust validates EventNonce uniqueness only within each authenticated author's slice, but the frontend globally deduplicates events by nonce alone, letting a hostile author overwrite another author's event in the rendered view.
+
+#### Root Cause
+
+The frontend treats an author-scoped random nonce as a globally unique identifier.
+
+**Nonce uniqueness is per author snapshot** — `crates/lanspread-peer/src/call_to_play.rs:961-979`
+
+Equal nonces across authenticated authors remain valid.
+
+```rust
+let mut event_ids = HashSet::with_capacity(snapshot.events.len());
+...
+if !event_ids.insert(event.id) {
+ return Err(CallToPlayValidationError::DuplicateEventId(event.id));
+}
+```
+
+**Rust projection preserves the namespace** — `crates/lanspread-peer/src/call_to_play.rs:877-884`
+
+The correct composite identity is available to consumers.
+
+```rust
+output.push(CallToPlayViewEvent {
+ id: event.id,
+ call_id: event.call_id,
+ author_id,
+ author_name: snapshot.display_name.clone(),
+ ...
+});
+```
+
+**Frontend drops the author namespace** — `crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts:135-145`
+
+A later event with the same nonce replaces another author's event before grouping.
+
+```typescript
+const unique = new Map(input.map(event => [event.id, event]));
+const byCall = new Map();
+for (const event of unique.values()) { ... }
+```
+
+#### Validation
+
+Validated. Creator-only Rust authorization remains effective, so severity is low and limited to rendered-state integrity/availability.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Nonce uniqueness is per author snapshot** — `crates/lanspread-peer/src/call_to_play.rs:961-979`
+
+Equal nonces across authenticated authors remain valid.
+
+```rust
+let mut event_ids = HashSet::with_capacity(snapshot.events.len());
+...
+if !event_ids.insert(event.id) {
+ return Err(CallToPlayValidationError::DuplicateEventId(event.id));
+}
+```
+
+**Rust projection preserves the namespace** — `crates/lanspread-peer/src/call_to_play.rs:877-884`
+
+The correct composite identity is available to consumers.
+
+```rust
+output.push(CallToPlayViewEvent {
+ id: event.id,
+ call_id: event.call_id,
+ author_id,
+ author_name: snapshot.display_name.clone(),
+ ...
+});
+```
+
+**Frontend drops the author namespace** — `crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts:135-145`
+
+A later event with the same nonce replaces another author's event before grouping.
+
+```typescript
+const unique = new Map(input.map(event => [event.id, event]));
+const byCall = new Map();
+for (const event of unique.values()) { ... }
+```
+
+Assertions:
+- Hostile authors can observe shared event nonces.
+- Cross-author duplicates survive Rust validation.
+- Frontend Map overwrite occurs before call grouping.
+
+Counterevidence and remaining uncertainty:
+- Author identity is TLS-derived and preserved.
+- Creator-only actions cannot be forged.
+- Impact is confined to UI state.
+
+#### Dataflow
+
+Pinned remote snapshot -\> authenticated Rust view with author_id -\> frontend Map keyed only by event.id -\> overwritten legitimate event.
+
+- **Source:** Attacker-controlled CallToPlayAuthorEvent.id.
+
+- **Sink:** Call-to-Play reducer and rendered nominations/messages.
+
+- **Outcome:** Legitimate calls or updates disappear or are substituted.
+
+**Nonce uniqueness is per author snapshot** — `crates/lanspread-peer/src/call_to_play.rs:961-979`
+
+Equal nonces across authenticated authors remain valid.
+
+```rust
+let mut event_ids = HashSet::with_capacity(snapshot.events.len());
+...
+if !event_ids.insert(event.id) {
+ return Err(CallToPlayValidationError::DuplicateEventId(event.id));
+}
+```
+
+**Rust projection preserves the namespace** — `crates/lanspread-peer/src/call_to_play.rs:877-884`
+
+The correct composite identity is available to consumers.
+
+```rust
+output.push(CallToPlayViewEvent {
+ id: event.id,
+ call_id: event.call_id,
+ author_id,
+ author_name: snapshot.display_name.clone(),
+ ...
+});
+```
+
+**Frontend drops the author namespace** — `crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts:135-145`
+
+A later event with the same nonce replaces another author's event before grouping.
+
+```typescript
+const unique = new Map(input.map(event => [event.id, event]));
+const byCall = new Map();
+for (const event of unique.values()) { ... }
+```
+
+#### Reachability
+
+Requires a connected hostile peer that observes the shared snapshot and publishes a later valid author slice.
+
+- **Attacker:** Authenticated hostile LAN peer.
+
+- **Entry point:** HelloSnapshot Call-to-Play author slice.
+
+Preconditions:
+- Attacker observes target nonce
+- Attacker publishes a colliding valid event
+
+Existing controls:
+- Per-author duplicate rejection
+- Creator-only action checks
+- Authenticated author attribution
+
+**Nonce uniqueness is per author snapshot** — `crates/lanspread-peer/src/call_to_play.rs:961-979`
+
+Equal nonces across authenticated authors remain valid.
+
+```rust
+let mut event_ids = HashSet::with_capacity(snapshot.events.len());
+...
+if !event_ids.insert(event.id) {
+ return Err(CallToPlayValidationError::DuplicateEventId(event.id));
+}
+```
+
+**Rust projection preserves the namespace** — `crates/lanspread-peer/src/call_to_play.rs:877-884`
+
+The correct composite identity is available to consumers.
+
+```rust
+output.push(CallToPlayViewEvent {
+ id: event.id,
+ call_id: event.call_id,
+ author_id,
+ author_name: snapshot.display_name.clone(),
+ ...
+});
+```
+
+**Frontend drops the author namespace** — `crates/lanspread-tauri-deno-ts/src/lib/callToPlay.ts:135-145`
+
+A later event with the same nonce replaces another author's event before grouping.
+
+```typescript
+const unique = new Map(input.map(event => [event.id, event]));
+const byCall = new Map();
+for (const event of unique.values()) { ... }
+```
+
+#### Severity
+
+**Low** — The attack can hide or substitute UI collaboration state but cannot forge authenticated author authority, modify files, or execute code.
+
+Impact grows if Call-to-Play state later triggers privileged actions automatically.
+
+Impact assessment:
+- **Level:** low
+- **Rationale:** Only ephemeral collaboration UI integrity is affected.
+
+Likelihood assessment:
+- **Level:** high
+- **Rationale:** The nonce is visible to connected peers and can be copied exactly.
+
+#### Remediation
+
+Use `(author_id, event.id)` as the key for deduplication, messages, React keys, and ordering; add cross-author collision tests including collision with a creator's Create event.
+
+
+
+### [15] Forged mDNS candidates can monopolize discovery slots
+
+| Field | Value |
+| --- | --- |
+| Severity | low |
+| Confidence | high |
+| Confidence rationale | Pre-auth admission, full-set rejection, and uniqueness keys are explicit. |
+| Category | resource-exhaustion |
+| CWE | CWE-400 |
+| Affected lines | crates/lanspread-peer/src/services/discovery.rs:30-50, crates/lanspread-peer/src/services/discovery.rs:233-259, crates/lanspread-peer/src/services/discovery.rs:294-309 |
+
+#### Summary
+
+Sixty-four unique attacker-controlled PeerId/address hints occupy every active and recent candidate slot before TLS proof, causing legitimate new candidates to be dropped while the attacker rotates records.
+
+#### Root Cause
+
+First-come discovery capacity is keyed only by unauthenticated claimed PeerId and full socket address, with no origin fairness.
+
+**Attacker controls both uniqueness keys** — `crates/lanspread-peer/src/services/discovery.rs:30-50`
+
+Distinct IDs and ports from one host fill the entire budget.
+
+```rust
+const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;
+...
+endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr
+...
+|| self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES
+```
+
+**Work starts from claimed fields** — `crates/lanspread-peer/src/services/discovery.rs:233-259`
+
+TLS proof happens after the slot is occupied.
+
+```rust
+if let Some(endpoint) = validated_candidate_endpoint(&info) {
+ ...
+ let handshake = ReservedCandidateHandshake::reserve(handshake_ctx, endpoint).await?;
+ active_candidates.insert(endpoint);
+ negotiations.push(run_protocol_negotiation(...));
+}
+```
+
+**Full set drops every later candidate** — `crates/lanspread-peer/src/services/discovery.rs:300-309`
+
+No fair queue or per-origin reserve remains for legitimate candidates.
+
+```rust
+active.len() < MAX_ACTIVE_DISCOVERY_CANDIDATES
+ && !candidate_conflicts(active, candidate)
+ && recent.try_record(candidate, now)
+```
+
+#### Validation
+
+Validated with low severity. The candidate cap protects memory but not fair availability.
+
+Validation method: Independent static source trace against the registered revision and strongest counterevidence.
+
+- **Status:** validated
+- **Disposition:** reportable
+
+**Attacker controls both uniqueness keys** — `crates/lanspread-peer/src/services/discovery.rs:30-50`
+
+Distinct IDs and ports from one host fill the entire budget.
+
+```rust
+const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;
+...
+endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr
+...
+|| self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES
+```
+
+**Work starts from claimed fields** — `crates/lanspread-peer/src/services/discovery.rs:233-259`
+
+TLS proof happens after the slot is occupied.
+
+```rust
+if let Some(endpoint) = validated_candidate_endpoint(&info) {
+ ...
+ let handshake = ReservedCandidateHandshake::reserve(handshake_ctx, endpoint).await?;
+ active_candidates.insert(endpoint);
+ negotiations.push(run_protocol_negotiation(...));
+}
+```
+
+**Full set drops every later candidate** — `crates/lanspread-peer/src/services/discovery.rs:300-309`
+
+No fair queue or per-origin reserve remains for legitimate candidates.
+
+```rust
+active.len() < MAX_ACTIVE_DISCOVERY_CANDIDATES
+ && !candidate_conflicts(active, candidate)
+ && recent.try_record(candidate, now)
+```
+
+Assertions:
+- No private key is required to occupy a slot until handshake fails.
+- All later candidates are rejected at 64 active entries.
+- An attacker can rotate IDs/ports after deadlines.
+
+Counterevidence and remaining uncertainty:
+- Work is capped and timed.
+- Duplicate IDs/addresses coalesce.
+- No state commits without pinned TLS.
+
+#### Dataflow
+
+mDNS TXT/address -\> PeerEndpoint -\> active candidate set -\> pinned negotiation -\> failure/repeat.
+
+- **Source:** Attacker-controlled multicast advertisements.
+
+- **Sink:** Discovery candidate capacity.
+
+- **Outcome:** Legitimate new peers are not discovered.
+
+**Attacker controls both uniqueness keys** — `crates/lanspread-peer/src/services/discovery.rs:30-50`
+
+Distinct IDs and ports from one host fill the entire budget.
+
+```rust
+const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;
+...
+endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr
+...
+|| self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES
+```
+
+**Work starts from claimed fields** — `crates/lanspread-peer/src/services/discovery.rs:233-259`
+
+TLS proof happens after the slot is occupied.
+
+```rust
+if let Some(endpoint) = validated_candidate_endpoint(&info) {
+ ...
+ let handshake = ReservedCandidateHandshake::reserve(handshake_ctx, endpoint).await?;
+ active_candidates.insert(endpoint);
+ negotiations.push(run_protocol_negotiation(...));
+}
+```
+
+**Full set drops every later candidate** — `crates/lanspread-peer/src/services/discovery.rs:300-309`
+
+No fair queue or per-origin reserve remains for legitimate candidates.
+
+```rust
+active.len() < MAX_ACTIVE_DISCOVERY_CANDIDATES
+ && !candidate_conflicts(active, candidate)
+ && recent.try_record(candidate, now)
+```
+
+#### Reachability
+
+Same multicast LAN while discovery is enabled.
+
+- **Attacker:** Hostile LAN participant.
+
+- **Entry point:** mDNS browser service observations.
+
+Preconditions:
+- Discovery enabled
+- Ability to advertise 64 unique records continuously
+
+Existing controls:
+- 64 active/recent cap
+- 5-second cooldown
+- Pinned TLS before commit
+
+**Attacker controls both uniqueness keys** — `crates/lanspread-peer/src/services/discovery.rs:30-50`
+
+Distinct IDs and ports from one host fill the entire budget.
+
+```rust
+const MAX_ACTIVE_DISCOVERY_CANDIDATES: usize = 64;
+...
+endpoint.peer_id == candidate.peer_id || endpoint.addr == candidate.addr
+...
+|| self.entries.len() >= MAX_ACTIVE_DISCOVERY_CANDIDATES
+```
+
+**Work starts from claimed fields** — `crates/lanspread-peer/src/services/discovery.rs:233-259`
+
+TLS proof happens after the slot is occupied.
+
+```rust
+if let Some(endpoint) = validated_candidate_endpoint(&info) {
+ ...
+ let handshake = ReservedCandidateHandshake::reserve(handshake_ctx, endpoint).await?;
+ active_candidates.insert(endpoint);
+ negotiations.push(run_protocol_negotiation(...));
+}
+```
+
+**Full set drops every later candidate** — `crates/lanspread-peer/src/services/discovery.rs:300-309`
+
+No fair queue or per-origin reserve remains for legitimate candidates.
+
+```rust
+active.len() < MAX_ACTIVE_DISCOVERY_CANDIDATES
+ && !candidate_conflicts(active, candidate)
+ && recent.try_record(candidate, now)
+```
+
+#### Severity
+
+**Low** — The attack suppresses discovery of new peers but is same-LAN, requires sustained advertisements, and cannot commit state or affect already known authenticated peers.
+
+Severity rises if discovery is the only operational path and attacks persist throughout events.
+
+Impact assessment:
+- **Level:** medium
+- **Rationale:** New peer discovery is denied; existing state and content remain protected.
+
+Likelihood assessment:
+- **Level:** high
+- **Rationale:** Only forged advertisements are required.
+
+#### Remediation
+
+Add per-source-IP quotas, preserve capacity across origins/known peers, and use fair/randomized replacement or a cheap proof-of-key before the longer negotiation slot.
+
+## Reviewed Surfaces
+
+| Surface | Risk Area | Outcome | Notes |
+| --- | --- | --- | --- |
+| Peer identity, authenticated endpoint generations, and aggregate remote state | Identity and state resource limits | Reported | Reported aggregate Sybil-state exhaustion; responder pinning, session/generation fencing, and content authority otherwise held. |
+| Anonymous QUIC admission, control frames, bulk transfers, and native StreamInstall work | Remote resource exhaustion | Reported | Reported origin-unfair global pools/native extractor fanout and eager length-prefix allocation. |
+| mDNS discovery, state hints, and liveness reconciliation | Discovery availability and confused deputy | Reported | Reported pre-auth candidate monopoly and unbound third-party pull hints; pinned follow-up preserves state integrity. |
+| Catalog transfer admission, manifest cache, ordinary downloads, and retries | Transfer resource budgets | Reported | Reported pre-admission manifest caching and Sybil-multiplied per-chunk deadlines; chunk/path/hash verification held. |
+| Download confinement, ordinary install/update/uninstall, Stream Install, and recovery | Installed-content integrity | Reported | Reported uncatalogued root archive extraction. No remote path escape, chunk hash bypass, or Stream Install output bypass found. |
+| Call-to-Play protocol, author ownership, frontend reduction, and rendering | UI integrity and availability | Reported | Reported cross-author EventNonce collision and maximal active-call rendering cost; creator authority and responder attribution held. |
+| Tauri commands, capabilities, storage, logs, and Windows process launch | Native privilege boundary | Reported | Reported runas of mutable game scripts. No hostile-peer XSS, raw HTML/eval, arbitrary thumbnail/log read, or remote capability grant found. |
+| Catalog publisher, package traversal, just recipes, build gates, and fixture/production separation | Build and publication authority | Reported | Reported shell interpolation and bounded out-of-root preflight reads. Atomic publication and production fixture gate held. |
+| Game-directory monitoring, migration, persisted indexes, and public per-game state helpers | Local filesystem input | Reported | Reported unbounded local ingestion/migration work and public marker path traversal. |
+| Protocol-v8 codecs, canonical IDs/paths, strict JSON, and legacy rejection | not recorded | No issue found | Strict current-only shapes, canonical path/ID types, unknown-field rejection, and semantic domain validation were traced; the aggregate decoder allocation issue is separately reported. |
+| Installation key persistence and secret handling | not recorded | No issue found | Bounded strict identity parsing, certificate/key/SPKI/SAN consistency, no-follow/no-clobber persistence, Unix private mode, and redacted diagnostics were verified. |
+| Cancellation, task/process ownership, generation shutdown, and crash recovery | not recorded | No issue found | Owned child tasks, process reaping, network admission closure, operation drain, version/install intents, and quarantine paths were traced with no detached product mutation path found. |
+| Frontend rendering, URLs, CSS, async ownership, and auxiliary windows | not recorded | No issue found | Remote values are React-escaped and no raw HTML, eval, javascript URL, or remote navigation sink was found; CSP null and broad app permissions remain defense-in-depth concerns. |
+| Peer CLI, Docker/just/Python scenario harnesses, and integration tests | not recorded | No issue found | Developer-authority boundaries, argv construction, cleanup scopes, fixture separation, and async lifecycle tests were reviewed; local stdin self-exhaustion was not treated as a security boundary. |
+| Discovery length-prefix allocation candidate | not recorded | Rejected | Merged into `resource-exhaustion.length-prefix-eager-reserve`; same locked decoder, aggregate-memory failure, evidence, and remediation. Evidence: discovery.rs:30-259; network.rs:98-215. |
+| Missing application-command ACL candidate | not recorded | Rejected | No source-backed attacker-controlled script execution exists in either fixed local log window; fixed local URLs and React escaping are counterevidence. Retained as least-privilege hardening. |
+| Broad main-window plugin authority candidate | not recorded | Rejected | Requires a compromised main renderer, already intentionally trusted with native custom commands; no current injection/navigation route establishes the prerequisite. |
+| Persistent Sybil peer/author slots candidate | not recorded | Rejected | Merged into `resource-exhaustion.sybil-aggregate-state`; same identity multiplication, retained-state budget, unbounded publication, and remediation. |
+| Unbounded peer CLI input candidate | not recorded | Rejected | Local stdin controller already owns the developer harness and can block/terminate it; no lower-privilege or remote boundary is crossed. Evidence: peer-cli/main.rs:167-180,409-428. |
+| Windows publisher TOCTOU candidate | not recorded | Rejected | Merged into `path-traversal.catalog-preflight-links`; same path-based read/rooted-handle failure and remediation. Evidence: catalog_publisher/package.rs:352-425. |
+| Markerless local uninstall candidate | not recorded | Rejected | Explicit authorized uninstall semantics: `local/` defines installed state and user-invoked uninstall intentionally removes it; marker creation proves the renamed backup is transaction-owned, not prior install provenance. |
+| Markerless local update candidate | not recorded | Rejected | Explicit authorized update semantics: update intentionally replaces existing `local/`, preserves it as backup until successful promotion, and restores on failure. |
+| Path-only ownership generation candidate | not recorded | Rejected | No meaningful attacker capability gain: an actor able to replace the entire game root already controls those files, and source tests intentionally preserve path ownership across recreation of the same configured root. |
+| StreamInstall subprocess fanout candidate | not recorded | Rejected | Merged into `resource-exhaustion.anonymous-network-pools` as the expensive bulk operation enabled by the same absent per-origin/costly-work quota. |
+| Legacy library-index migration candidate | not recorded | Rejected | Merged into `resource-exhaustion.local-library-ingestion`; same selected-filesystem byte/cardinality budget gap and remediation. |
+| Sybil peers can exhaust aggregate state and unbounded UI publication | not recorded | Reported | Validated finding 1. |
+| Anonymous LAN requesters can monopolize global pools and native extractors | not recorded | Reported | Validated finding 2. |
+| Rejected bulk requests can populate the persistent manifest cache | not recorded | Reported | Validated finding 3. |
+| Control-frame prefixes can reserve about 512 MiB across concurrent decoders | not recorded | Reported | Validated finding 4. |
+| Mutable game scripts are launched elevated without launch-time trust binding | not recorded | Reported | Validated finding 5. |
+| Cross-author event-ID collisions can suppress Call-to-Play entries | not recorded | Reported | Validated finding 6. |
+| Unauthenticated hints can make the victim pull arbitrary known peers | not recorded | Reported | Validated finding 7. |
+| Forged mDNS candidates can monopolize discovery slots | not recorded | Reported | Validated finding 8. |
+| Selected game directories can trigger unbounded monitoring and migration work | not recorded | Reported | Validated finding 9. |
+| Catalog/build recipe arguments are interpolated as shell code | not recorded | Reported | Validated finding 10. |
+| Catalog preflight can read outside the package root through links and Windows races | not recorded | Reported | Validated finding 11. |
+| Ordinary install extracts uncatalogued root archives without output verification | not recorded | Reported | Validated finding 12. |
+| Discovery prefix allocation | not recorded | Rejected | Merged into the reportable length-prefix finding: same locked decoder, aggregate-memory failure, and remediation; the discovery path is preserved as an affected entry point. |
+| Application command ACL | not recorded | Rejected | Not separately reportable: no source-backed attacker-controlled script execution exists in either fixed local log window; React text rendering and fixed URLs are counterevidence. Retained as least-privilege hardening. |
+| Main-window plugin authority | not recorded | Rejected | Not separately reportable: it requires a compromised main renderer, for which the application intentionally exposes a broad native command surface; no current injection/navigation route establishes that prerequisite. |
+| Sybil peer/author slots | not recorded | Rejected | Merged into the reportable aggregate Sybil-state finding: same first-come identity admission, retained-state budget, unbounded full-view publication, and remediation. |
+| Unbounded peer CLI input | not recorded | Rejected | Rejected as self-only developer-harness behavior: the local stdin controller already owns the harness and can terminate or block it; no lower-privilege or remote boundary is crossed. |
+| Windows publisher TOCTOU | not recorded | Rejected | Merged into the publisher link/TOCTOU finding because both arise from path-based reads without rooted no-follow identity and share the same remediation. |
+| Uninstall markerless local | not recorded | Rejected | Rejected as explicit authorized uninstall semantics. `local/` defines installed state and the user-invoked uninstall intentionally removes it; `.lanspread_owned` is added so the renamed backup is transaction-owned, not as prior install provenance. |
+| Update markerless local | not recorded | Rejected | Rejected as explicit authorized update semantics. A user-invoked update intentionally replaces an existing `local/` tree while preserving it as backup until promotion succeeds. |
+| Path-only root generation ownership | not recorded | Rejected | Rejected for lack of meaningful attacker capability gain: a same-authority actor able to replace the whole game root already controls those files, and tests/documentation intentionally preserve path ownership across recreation of the same configured root. |
+| StreamInstall subprocess fanout | not recorded | Rejected | Merged into the anonymous global-pools finding as the expensive bulk operation enabled by the same missing per-origin/global costly-work quota. |
+| Legacy index migration | not recorded | Rejected | Merged into the local-library ingestion finding because it is another whole-file/cardinality budget gap on the same selected-filesystem boundary. |
+| Independent baseline audit | not recorded | Needs follow-up | Two candidates. |
+| Network, TLS, protocol, and transfer admission | not recorded | Needs follow-up | Three candidates. |
+| Tauri commands, frontend, windows, and process launch | not recorded | Needs follow-up | Three candidates. |
+| Discovery, liveness, peer state, and Call-to-Play | not recorded | Needs follow-up | Four candidates. |
+| Peer CLI, local library monitor, and runtime glue | not recorded | Needs follow-up | Two candidates. |
+| Catalog publisher, build gates, and package traversal | not recorded | Needs follow-up | Three candidates. |
+| Download confinement, install/update/uninstall, streamed install, and recovery | not recorded | Needs follow-up | Seven candidates. |
+| One peer can force continuous rendering of thousands of active calls | not recorded | Reported | Validated residual frontend finding. |
+| Sybil sources can multiply one chunk deadline into a multi-hour retry loop | not recorded | Reported | Validated residual download finding. |
+| Residual download planning, retry, progress, and drain modules | not recorded | Needs follow-up | Retry-budget candidate remains pending. |
+| Residual production frontend components and reducers | not recorded | Needs follow-up | One semantic rendering-budget candidate checkpointed before validation. |
+| Public state helpers permit marker writes outside the state directory | not recorded | Reported | Validated public API path-traversal finding. |
+| Residual peer-core lifecycle, state paths, quarantine, startup, and façades | not recorded | Needs follow-up | One public state-path candidate checkpointed before validation. |
+
+## Open Questions And Follow Up
+
+- The external 186-game production package corpus and generated production manifests are absent, so their byte provenance and aggregate manifest-memory size could not be verified.
+ - Follow-up prompt: Re-run catalog provenance and manifest-size review with the retained production corpus and generated manifests.
+- Checked-in unrar sidecars are opaque binaries with no checked-in upstream version/source/signature provenance.
+ - Follow-up prompt: Establish vendor release/source mapping and reproducible or signed provenance for each platform sidecar.
+- This was an offline static scan; physical-LAN behavior, Windows UAC/runtime paths, and live memory/DOM exhaustion were not reproduced.
+ - Follow-up prompt: Run bounded adversarial integration tests on representative Windows and physical-LAN hosts.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-legacy-index-unbounded-migration and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-stream-install-subprocess-fanout and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-ownership-root-generation and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-update-adopts-foreign-local and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-uninstall-adopts-foreign-local and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-windows-package-toctou and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-cli-unbounded-jsonl and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-sybil-peer-author-slots and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-main-window-plugin-authority and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-app-command-acl and close its stated proof gap.
+- Pending parent validation.
+ - Follow-up prompt: Review deferred unit cand-discovery-prefix-allocation and close its stated proof gap.
diff --git a/security-report/scan-manifest.json b/security-report/scan-manifest.json
new file mode 100644
index 0000000..11a3cab
--- /dev/null
+++ b/security-report/scan-manifest.json
@@ -0,0 +1,151 @@
+{
+ "documentType": "codex-security.scan-manifest",
+ "scan": {
+ "artifacts": [
+ {
+ "mediaType": "application/json",
+ "path": "findings.json",
+ "sha256": "9bd9fa7e2fa8071bfe484e1a8da7fb0e328e1f7943aec9dfd43001d5ca8fc8c9"
+ },
+ {
+ "mediaType": "application/json",
+ "path": "coverage.json",
+ "sha256": "4bb4b678a4815bad530e56b6779babc7ed0ced0d303018d3a2bbb0f3907487c4"
+ }
+ ],
+ "completedAt": "2026-08-28T12:39:47.040071Z",
+ "coverageRef": "coverage.json",
+ "findingsRef": "findings.json",
+ "id": "916f0812-d79c-465f-a422-81d75a258f76",
+ "preservedSources": {
+ "checkpoints/0960fa25031467cecbd5ba68970b4edb554771ef51a4cef8c7bd71926e23cb63.json": "b8841d94b92fd266352fe18e6f3958bf80263d1abedc4c5daba9a1d1aac8a451",
+ "checkpoints/0fbb155ed511da4a98dccdd17371789e2c34b1b1f82fe497016d6867be2577b4.json": "6249b8ae681b4c5eb2ede49879953cba91e98acdea32049461ba18d75dc0d6fe",
+ "checkpoints/0fd709dc3807bddae018201f02af7c9b34ed4a7ff76cb7e90368f859f700256d.json": "daacaac200a4e98c864f02faa3987a2b685d5010fa4ea5df48fa6ef2fbabe765",
+ "checkpoints/1198ded03a33af08cf5cb3a997280d52de9a9a91b1d50f4fe657a9ea8fe82025.json": "0811fd21b0ff29b67cac9a8ceb1b9d42d7d612eb5a4335b3c9b6790829a1dfdb",
+ "checkpoints/19527da2ce2f311bd9db83f5a200b5841b098ac4fba79c97e8bff6b75d3049a1.json": "1d192f3008c228122af449281f2c2df81085ab5854cbf7aff24be19022a93953",
+ "checkpoints/1ad656ee1361c869589d82279e7b2a3161dbd5f0202ccb5b515d956e6660be03.json": "279362fd0a086d267719a6a789734d1d44d2358db181a1557bb436362b9ec272",
+ "checkpoints/1f5c1ac3f9fec04c8b9eb3d0a0f5daeddfe0b50e1b592eeaf5a13ec732b3f88f.json": "c2dd7100b75dbe4324701f57ff4874d5811add0e260c8cfdcf543548899fe126",
+ "checkpoints/2c06d77bfab6ef8166c300b7848deb9e532909343947d75e59cac3f06c0b59b3.json": "24caa5ca0889f160fbe62e4a3e2a8d66bd43e71b619da8ab8bb58f2ac0a8a217",
+ "checkpoints/2d398ac02228e1f75e8858750413ea1e2e0a94035151821aaee714d32c5cd813.json": "7aedb83e3725da99a3602ad97909e3e3733667c0192961f69206e8e28dab5a2c",
+ "checkpoints/31c4c30c171100235e15ae747e02364e7765505eb6e15f3da1fc6b571f668898.json": "c9f69d794b03c24cf184cbdb18ccfd3c9fc05b6869a7549d69100c678c1671b5",
+ "checkpoints/32ad00e27602adbbf7e07a2dbbefbf3ec619168d9993605cc22613c631b70ed6.json": "0243234f06f00bb7424ca3815cb6f2c695a0ed1cf7d521c47b06cbcc301c4416",
+ "checkpoints/3772cfe891f312df9f5c893d15ded40b802e96256be44764bd1c58390e6a49a7.json": "2b7522696fd05018bd0890a13af90fd40b2287992c5ff10bcddbe8ef1d4b1edf",
+ "checkpoints/3b141e38b5d51b92b653eb9855de8f410263a2cb26c27c737ec315a3b2d7d2bc.json": "e6a8334569f1a4e20d9fab3b41c711e81d1f326bb270d8667bc4e3da5079fc7c",
+ "checkpoints/4c82e4452b38d0beda4ac3032794efc00f080112d2cc56cbe0a121b943b33592.json": "98354fd6ddfec954fb4031654b923b4fb8806efee9695d8e1a1027f72b67e263",
+ "checkpoints/530fe41e4de609c4873bbc6e932d9d20e149fa46df831c47c5d79d1b77c1f60c.json": "62d9a7922393969c529d22bf8681503bcd54b5600a11fd0a38e7e933683981f5",
+ "checkpoints/5a025ca99ea1abb4f07d52b96832c9dcec2582a0f682c8f3a3e65d3c0defd764.json": "075f0a6620082e397d1ae5dc2ee2dfcaf5e77dee38ae8bd2b04ad6bc72ae8d4b",
+ "checkpoints/6579dbda136279e85da256440c399d074b88dc1228957c4d67c17971539dbcf5.json": "96871be42491f1d51b5b5149285b2266cee7e5698e6197aa00c3531d210c168e",
+ "checkpoints/66245413c3942c393b867f97f55b1a6e3564cf89828ee66946280f692f3dd52e.json": "a98b57bdb4953f0427e26e74842361f7ee64a7e21317b15e32e92a58428e386d",
+ "checkpoints/781fcc6261b915ed6e8e614ccc66b97c364dc353da8476f19f257b55105293ec.json": "952273dab36240e477c880108c031bc7780d00b2bd931dd856c63495aa49f2b4",
+ "checkpoints/8136e92497c107bff2f0feaed190151f9debb63448c55cd1fb34e43d471210be.json": "bbb5a1028a7931c38a8532af9988b833b4dcd5a9b20c21014d767ff263492d34",
+ "checkpoints/84393bfd53f6dcdb72a526775ea703f507418e7f3f418744a0b6cc2f26cba228.json": "9db4e5de939efd67d177b40008a892de9e577e342d3d266c24fcce8fde7c9d68",
+ "checkpoints/84c03716e3dfb359f3c9c76d151612d1e76bf4b71e8c0c65b7abd5b170ba3220.json": "84c03716e3dfb359f3c9c76d151612d1e76bf4b71e8c0c65b7abd5b170ba3220",
+ "checkpoints/a9960a026fb7b1bd6575901ca2d6398e6cbd82bdd6f7baf9fd14e7aac1c65666.json": "61035ad19aecef054db4ed2ca0bbe172b033a7ca596ec665f475d67dee7c4a67",
+ "checkpoints/a9eba02bc2d2460f1332903837fb063bd4801da2483d1563afe2ea5cf75b72f9.json": "a6b297b43f4fff177d9781151d98e7f059b5f191845be41b8099d7eacd492301",
+ "checkpoints/ba1907e02f4dba118a86a4c91d9884ea8d4aab47d4fcfb0b7b12dd7653ba2300.json": "5f5d39ee99fb027788eecf8773c9a21ec94179ff41716b14df90a5c21f5eb2ed",
+ "checkpoints/bf301f5b9462daf8075b3f815bf1ccde672ceab2388d6091aac8a332d7334fca.json": "b4555c42ea3ee40406a947f552c728a5269a6c2045369d0760e9da163a94aa75",
+ "checkpoints/c1592ebeace399879bfb08480309c322aa16fb8df984658777300b63b0e6518d.json": "938bfeaf26254f6c236d9a257c144d3b56c64585e544d217cea241bf976dedaa",
+ "checkpoints/c7f9309df25d5245680b4c10638b63463205a3f557343e33fc8a0234d355d046.json": "ec86a7587b861839de3bbf58f5c2dae05068c464d1858aedceb98095bd1cf3d6",
+ "checkpoints/d7abddf776f4cd66dfbd492df542bdbc340e4fad2e5cc00cd3f6fd602ba484a2.json": "8c49c9dc1a8cbf0de524fe4579afcda77b79bb3565daad8f367b58b9667fae20",
+ "checkpoints/dfe517b4bc35705d482d9163e6e8d6455afa8c6fc58332eea24e4502d24ea0bc.json": "d8956db684115d911ca5ebde1f733f0589b5f539729461a76a1788592f7aeefc",
+ "checkpoints/e8b9b3d8abf8a4240962554eef159c0a1c3882e7ad46be66fddc4be2b4be70c2.json": "331da61831d3191b1c444dfe8e3c8b0b6e5ef91cbdcfaca8bc9f4d8171fab9da",
+ "checkpoints/e98db1859f34fab50026fb15a7593a156c6e16ad98eb6e17cb548f85cc191161.json": "c4f470db8d00ea8c101cc3a175a536986e2bbace12720624932db37b4b7af0b0",
+ "checkpoints/ec4ccca6f071f5412a781a942d0ae7eba64e802c08c04d4e4f518124c7578e00.json": "1a4a18bd9e0a8e8c34c5f1e052cd3bfae6db57044f625c3069d50feb6fb5b1de",
+ "checkpoints/f14d288a32a8ab57ed93e6f9d2bdddd4c06c91e7aba449692672ad74b5d6e071.json": "7680e3d349bb60556eaea71ff3be83e5940bcb7627c1e7c6b8be7131008ea55b",
+ "checkpoints/f92bad62b95cfd47ccebfe290c2dc40530fbf200c7e90d18ac7e304444268fbf.json": "70ee6a976c59f29ae26046fafadc8017afc2d58152dd09a5a57e38465f3a30b8",
+ "checkpoints/fbdab722d42f63041c76e0e5f10c2a6bf749e1407e27dcf8b8c626b68a5b2d70.json": "bdb59f1b228daf7d86a8cf5355616b42d20240241e7dcfb76fff883a06471928"
+ },
+ "producer": {
+ "name": "codex-security-plugin",
+ "version": "0.1.22"
+ },
+ "scope": {
+ "artifactsReviewed": [
+ "94 Rust source files",
+ "production TypeScript/TSX/CSS/HTML frontend",
+ "Cargo/deno manifests and locks",
+ "Tauri capabilities/config/build gate",
+ "just/Python/Docker publisher and peer-CLI workflows",
+ "tests exposing product contracts",
+ "opaque unrar sidecar metadata/digests"
+ ],
+ "context": "Standard single-pass read-only security audit with no additional user focus.",
+ "excludePaths": [],
+ "includePaths": [
+ "."
+ ],
+ "limitations": [
+ "Offline static review only; no application or third-party binary execution.",
+ "No online dependency advisory lookup.",
+ "External production package corpus and generated production manifests absent.",
+ "No physical-LAN, Windows UAC, or live resource-exhaustion reproduction."
+ ],
+ "runtimeStatus": "Source review, validation, attack paths, and coverage reconciliation complete.",
+ "summary": "Whole Git-backed Lanspread repository at revision 15faecbf89735f277f208c84895464a5190b9236, including Rust workspace crates, Tauri frontend/shell, catalog/build tooling, peer CLI, tests, configs, generated permission manifests, and opaque sidecars as data.",
+ "validationMode": "Every worker candidate checkpointed before one parent source validation; duplicates merged only when root cause and remediation matched."
+ },
+ "sealedAt": "2026-08-28T12:39:47.040071Z",
+ "startedAt": "2026-08-28T11:50:39.443096Z",
+ "status": "completed",
+ "target": {
+ "displayName": "lanspread",
+ "kind": "git_revision",
+ "revision": "15faecbf89735f277f208c84895464a5190b9236",
+ "targetId": "target_sha256_b3f00671b54800aee0df5478bf1bd03061b69b79eb9beece1ed34dc8c8bbc8a6"
+ },
+ "threatModel": {
+ "assets": [
+ "Installation Ed25519 private key and stable PeerId in Tauri AppData or CLI state-dir. Sources: crates/lanspread-peer/src/state_paths.rs:18-36; identity.rs:61-165,223-291.",
+ "Catalog authority: game.db, compact content index, canonical manifest paths/sizes/BLAKE3 and Stream Install outputs. Sources: crates/lanspread-compat/src/catalog_bundle.rs:50-82,169-233; crates/lanspread-db/src/content_manifest/bundle.rs:20-124.",
+ "Configured game storage, user-owned local installs, version sentinel, staging/backup, install intent, and download ownership journals. Sources: crates/lanspread-peer/ARCHITECTURE.md:202-260.",
+ "Authenticated endpoint generations, runtime sessions, library/Call-to-Play projections, and application availability.",
+ "Tauri native command/plugin authority and Windows administrator process boundary.",
+ "Catalog package/build provenance and checked-in opaque unrar sidecars."
+ ],
+ "assumptions": [
+ "Requester anonymity while Local network sharing is enabled is intentional product behavior; responder identity, not membership, is authenticated.",
+ "Missing sharing policy defaults to enabled, while malformed/unreadable policy fails disabled and no game directory prevents peer startup.",
+ "The current production game.db has 186 rows but the production manifests and external package corpus are absent, so their aggregate size/provenance cannot be verified.",
+ "Tauri AppData/Resource absolute paths are platform-owned; logical child paths are source-backed.",
+ "Windows UAC policy, firewall/LAN segmentation, physical-LAN behavior and cross-user directory ACLs are deployment prerequisites.",
+ "Checked-in unrar binaries were inspected as opaque binary data and hashed, but source-to-binary/upstream provenance is unresolved.",
+ "Docker peer CLI and Python scenario tools are developer/test surfaces, not production requester APIs.",
+ "No source-backed frontend script injection, raw HTML, eval, or remote navigation path was found.",
+ "Windows game/server scripts are elevated by source but this privilege transition is not documented in README/ARCHITECTURE."
+ ],
+ "attackerCapabilities": [
+ "Same-LAN host may advertise arbitrary identities/endpoints, operate valid self-issued identities, send anonymous requests/hints, and control its own snapshots/transfer bytes, but not forge another PeerId or local catalog hashes.",
+ "Local/shared-filesystem actor may populate a user-selected games/package directory but does not initially control AppData, build operator credentials, or victim UAC decision.",
+ "Local CLI stdin controller is operator authority and is not modeled as a remote attacker.",
+ "Compromised renderer is conditional; no current hostile-peer XSS or remote-navigation route was established.",
+ "Package/CI input supplier may influence documented recipe arguments where an operator passes them through."
+ ],
+ "securityObjectives": [
+ "Pin every outbound responder to the expected Ed25519 SPKI-derived PeerId and TLS 1.3 proof.",
+ "When sharing is disabled, close admission, drain network generations, and clear remote projections.",
+ "Treat mDNS, hints and payload claims as non-authoritative until a pinned pull and current generation/session commit.",
+ "Accept only protocol 8 with strict bounded decoding and no compatibility fallback.",
+ "Keep remote bytes subordinate to local catalog ContentId/path/size/BLAKE3 authority.",
+ "Confine file reads/writes/deletes to canonical game roots and preserve unknown/user-owned content outside explicit operations.",
+ "Bound public work before allocation and apply fair quotas to attacker-multipliable identities/origins.",
+ "Keep fixture authority out of production builds and make catalog publication fail closed.",
+ "Bind elevated process launch to verified immutable content.",
+ "Keep process/task shutdown structured and joined."
+ ],
+ "summary": "Lanspread is a single-user LAN-party game-sharing desktop app plus a developer JSONL CLI and catalog publication tools. A sharing-enabled peer binds ephemeral IPv4 QUIC on all interfaces, advertises/discovers via mDNS, authenticates only the responder with SPKI-derived PeerId pinning, and serves requester-anonymous current-protocol operations. Remote availability and bytes remain subordinate to the bundled catalog. Sources: README.md:3-17; crates/lanspread-peer/src/tls.rs:51-143; crates/lanspread-peer/src/services/server.rs:74-177; crates/lanspread-peer/src/download/manifest.rs:129-185.",
+ "trustBoundaries": [
+ "Anonymous LAN requester -> QUIC server: TLS authenticates the responder only; global frame/task/transfer controls and sharing state govern admission. Sources: tls.rs:126-143; services/server.rs:31-365; services/stream.rs:34-303.",
+ "mDNS hint -> pinned endpoint: claimed PeerId/address occupies bounded candidate work; only a pinned full pull commits state. Sources: services/discovery.rs:30-61,233-259; services/remote_state.rs:147-275.",
+ "Pinned snapshot -> peer database and Call-to-Play: endpoint generation, runtime session, revision and independent domain validation gate commit.",
+ "Remote availability/bytes -> local catalog -> confined filesystem: peers never choose expected paths/hashes; complete catalog manifests and no-follow capabilities gate reads/writes.",
+ "Stream Install frames -> verified staging: catalog archive/output shape, size and digest must match before promotion; ordinary install is a materially different path.",
+ "Main/auxiliary Tauri webviews -> registered native commands/plugins; bundled content and localhost development content both have CSP null.",
+ "User-selected game directory -> backend canonical acknowledgement -> scanner/downloader/installer/Windows launcher.",
+ "Catalog/package operator -> publisher shell recipes -> Rust publisher/unrar -> production manifest/bundle authority.",
+ "Catalog or mutable game scripts -> Windows cmd.exe runas after local action/UAC.",
+ "AppData/state-dir -> identity, policy, logs, migration and recovery consumers."
+ ]
+ }
+ },
+ "schemaVersion": "1.0"
+}