From 00a914d7dde2645cf17614aebf9d824ecb1cf628 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 21:38:03 +0200 Subject: [PATCH 1/2] Pin cultcache-rs to bbb98eaf (0.3.0: stacker growth at every decode level) Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 2 +- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3898b9a..01e8ccb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -67,8 +76,8 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cultcache-rs" -version = "0.2.0" -source = "git+https://github.com/GameCult/CultLib.git?rev=b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7#b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7" +version = "0.3.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=bbb98eaf660e43ef611dc2f5132df99fe21e2f01#bbb98eaf660e43ef611dc2f5132df99fe21e2f01" dependencies = [ "anyhow", "chrono", @@ -79,14 +88,16 @@ dependencies = [ "serde", "serde_bytes", "serde_json", + "serde_stacker", + "stacker", "uuid", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "cultcache-rs-derive" -version = "0.1.0" -source = "git+https://github.com/GameCult/CultLib.git?rev=b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7#b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7" +version = "0.2.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=bbb98eaf660e43ef611dc2f5132df99fe21e2f01#bbb98eaf660e43ef611dc2f5132df99fe21e2f01" dependencies = [ "proc-macro2", "quote", @@ -212,6 +223,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -233,6 +253,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "quote" version = "1.0.47" @@ -335,6 +365,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_stacker" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4936375d50c4be7eff22293a9344f8e46f323ed2b3c243e52f89138d9bb0f4a" +dependencies = [ + "serde", + "serde_core", + "stacker", +] + [[package]] name = "shlex" version = "2.0.1" @@ -347,6 +388,19 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + [[package]] name = "syn" version = "2.0.119" @@ -532,6 +586,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index d4018b6..b8a875a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,6 @@ resolver = "2" [workspace.dependencies] anyhow = "1" -cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "b3bd8dd5e5b208b2012392e9c5d6dfee2441aaa7", package = "cultcache-rs" } +cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "bbb98eaf660e43ef611dc2f5132df99fe21e2f01", package = "cultcache-rs" } serde = { version = "1", features = ["derive"] } rmp-serde = "1.3" From 8d44605301e0b89f943fac5dd2473c6a7d3382e1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 14 Oct 2026 21:38:04 +0200 Subject: [PATCH 2/2] thing-contract: pin decode stack safety at the nesting bound Repeat chain (125 levels) on a 1 MiB thread and Box chain (83 levels) on a 2 MiB thread must decode; deep_tree no longer needs a 16 MiB thread. Co-Authored-By: Claude Sonnet 5.5 --- crates/thing-contract/tests/decode_stack.rs | 76 +++++++++++++++++++++ crates/thing-contract/tests/goldens.rs | 7 -- 2 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 crates/thing-contract/tests/decode_stack.rs diff --git a/crates/thing-contract/tests/decode_stack.rs b/crates/thing-contract/tests/decode_stack.rs new file mode 100644 index 0000000..8e98b90 --- /dev/null +++ b/crates/thing-contract/tests/decode_stack.rs @@ -0,0 +1,76 @@ +//! A surface payload inside CultCache's 256 nesting bound must decode on a small thread without +//! aborting the process. Soul's finding `thing2:finding:cut-contract-rs.s1.decode-stack-overflow- +//! within-bound` found a 2.8 KB Repeat chain aborting a 1 MiB thread in release at 68 levels and a +//! Box chain aborting a 2 MiB debug thread at 24 levels, while the bound admits 125 and 83. A +//! stack overflow kills the whole test binary, so these tests fail loudly on a decoder that +//! does not grow its stack. + +mod common; + +use common::*; +use cultcache_rs::decode_payload; +use thing_contract::*; + +const REPEAT_LEVELS: usize = 125; +const BOX_LEVELS: usize = 83; + +fn surface(root: Node) -> SurfaceDocument { + SurfaceDocument { + provider_id: ProviderId::new("provider.a"), + label: seg("deep"), + version: ver(1), + title: "deep".into(), + root, + commands: vec![], + } +} + +fn repeat_chain(levels: usize) -> SurfaceDocument { + let mut chain = text("leaf", "bottom"); + for depth in (0..levels).rev() { + chain = Node::Repeat(RepeatNode { + common: common(&format!("rep-{depth}")), + collection: CollectionSource::Param(param("rows")), + key_member: path(&["id"]), + template: std::boxed::Box::new(chain), + }); + } + surface(chain) +} + +fn box_chain(levels: usize) -> SurfaceDocument { + let mut chain = text("leaf", "bottom"); + for depth in (0..levels).rev() { + chain = boxed(&format!("box-{depth}"), vec![chain]); + } + surface(chain) +} + +/// Builds, encodes and drops the value on a roomy thread, so only the decode runs on `stack`. +fn decode_on_small_thread(build: fn() -> SurfaceDocument, stack: usize) { + let expected = std::thread::Builder::new().stack_size(64 << 20).spawn(build).unwrap().join().unwrap(); + let payload = std::thread::Builder::new() + .stack_size(64 << 20) + .spawn(move || (rmp_serde::to_vec(&expected).unwrap(), expected)) + .unwrap() + .join() + .unwrap(); + let (bytes, expected) = payload; + let decoded = std::thread::Builder::new() + .stack_size(stack) + .spawn(move || decode_payload::(&bytes).unwrap()) + .unwrap() + .join() + .unwrap(); + std::thread::Builder::new().stack_size(64 << 20).spawn(move || assert!(decoded == expected)).unwrap().join().unwrap(); +} + +#[test] +fn repeat_chain_at_the_bound_decodes_on_a_1_mib_thread() { + decode_on_small_thread(|| repeat_chain(REPEAT_LEVELS), 1 << 20); +} + +#[test] +fn box_chain_at_the_bound_decodes_on_a_2_mib_thread() { + decode_on_small_thread(|| box_chain(BOX_LEVELS), 2 << 20); +} diff --git a/crates/thing-contract/tests/goldens.rs b/crates/thing-contract/tests/goldens.rs index 71a5c23..9529874 100644 --- a/crates/thing-contract/tests/goldens.rs +++ b/crates/thing-contract/tests/goldens.rs @@ -665,13 +665,6 @@ fn refusal_cases() { #[test] fn deep_tree() { - // Decoding a Node costs tens of KiB of debug stack per nesting level, so the chain runs on a - // thread that has the room; the 2 MiB default does not. The hazard is CultCache's, owned by - // cultlib-gaps:follow_up:decode-stack-safety; the test stays on the big thread until it lands. - std::thread::Builder::new().stack_size(16 * 1024 * 1024).spawn(deep_tree_body).unwrap().join().unwrap(); -} - -fn deep_tree_body() { // A 64-deep Box chain. let mut chain = text("leaf", "bottom"); for depth in (0..64).rev() {