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 <noreply@anthropic.com>
This commit is contained in:
parent
00a914d7dd
commit
8d44605301
2 changed files with 76 additions and 7 deletions
76
crates/thing-contract/tests/decode_stack.rs
Normal file
76
crates/thing-contract/tests/decode_stack.rs
Normal file
|
|
@ -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::<SurfaceDocument>(&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);
|
||||||
|
}
|
||||||
|
|
@ -665,13 +665,6 @@ fn refusal_cases() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deep_tree() {
|
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.
|
// A 64-deep Box chain.
|
||||||
let mut chain = text("leaf", "bottom");
|
let mut chain = text("leaf", "bottom");
|
||||||
for depth in (0..64).rev() {
|
for depth in (0..64).rev() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue