#![cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use std::process::Command;
use openmls::prelude::{Credential, CredentialType, RatchetTreeIn};
use openmls::test_utils::frankenstein::{
FrankenCapabilities, FrankenExtension, FrankenLeafNode, FrankenLeafNodePayload,
FrankenLeafNodeSource, FrankenNode, FrankenRatchetTreeExtension,
};
use tls_codec::{Deserialize, Serialize};
const LEAF_PREFIX: &[u8] = &[
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, ];
const LEAF_SIGNATURE: u8 = 0x00;
const EXTENSION_TYPE_RATCHET_TREE: [u8; 2] = [0x00, 0x02];
fn var_len_prefix(len: usize) -> Vec<u8> {
if len <= 0x3f {
vec![len as u8]
} else if len <= 0x3fff {
(len as u16 | 0x4000).to_be_bytes().to_vec()
} else if len <= 0x3fff_ffff {
(len as u32 | 0x8000_0000).to_be_bytes().to_vec()
} else {
(len as u64 | 0xc000_0000_0000_0000).to_be_bytes().to_vec()
}
}
fn var_len_bytes(body: &[u8]) -> Vec<u8> {
let mut out = var_len_prefix(body.len());
out.extend_from_slice(body);
out
}
fn ratchet_tree_with_leaf(extensions_field: &[u8]) -> Vec<u8> {
let mut leaf = LEAF_PREFIX.to_vec();
leaf.extend_from_slice(extensions_field);
leaf.push(LEAF_SIGNATURE);
let mut node = vec![0x01];
node.extend_from_slice(&leaf);
let mut opt = vec![0x01];
opt.extend_from_slice(&node);
var_len_bytes(&opt)
}
fn base_ratchet_tree() -> Vec<u8> {
ratchet_tree_with_leaf(&var_len_bytes(&[]))
}
fn build_nested_ratchet_tree(depth: usize) -> Vec<u8> {
let base = base_ratchet_tree();
if depth == 0 {
return base;
}
let mut open_segments: Vec<Vec<u8>> = Vec::with_capacity(depth);
let mut inner_len = base.len();
for _ in 0..depth {
let body_prefix = var_len_prefix(inner_len);
let extension_len = EXTENSION_TYPE_RATCHET_TREE.len() + body_prefix.len() + inner_len;
let extensions_prefix = var_len_prefix(extension_len);
let leaf_len =
LEAF_PREFIX.len() + extensions_prefix.len() + extension_len + 1 ;
let node_len = 1 + leaf_len;
let opt_len = 1 + node_len;
let tree_prefix = var_len_prefix(opt_len);
let mut open = Vec::new();
open.extend_from_slice(&tree_prefix);
open.push(0x01); open.push(0x01); open.extend_from_slice(LEAF_PREFIX);
open.extend_from_slice(&extensions_prefix);
open.extend_from_slice(&EXTENSION_TYPE_RATCHET_TREE);
open.extend_from_slice(&body_prefix);
open_segments.push(open);
inner_len = tree_prefix.len() + opt_len;
}
let mut out = Vec::with_capacity(inner_len);
for open in open_segments.iter().rev() {
out.extend_from_slice(open);
}
out.extend_from_slice(&base);
out.extend(std::iter::repeat_n(LEAF_SIGNATURE, depth));
out
}
fn franken_leaf(extensions: Vec<FrankenExtension>) -> FrankenLeafNode {
FrankenLeafNode {
payload: FrankenLeafNodePayload {
encryption_key: vec![].into(),
signature_key: vec![].into(),
credential: Credential::new(CredentialType::Basic, vec![]).into(),
capabilities: FrankenCapabilities {
versions: vec![],
ciphersuites: vec![],
extensions: vec![],
proposals: vec![],
credentials: vec![],
},
leaf_node_source: FrankenLeafNodeSource::Update,
extensions,
},
signature: vec![].into(),
}
}
fn build_nested_ratchet_tree_frankenstein(depth: usize) -> Vec<u8> {
let mut tree: Vec<Option<FrankenNode>> =
vec![Some(FrankenNode::LeafNode(franken_leaf(vec![])))];
for _ in 0..depth {
let extension =
FrankenExtension::RatchetTree(FrankenRatchetTreeExtension { ratchet_tree: tree });
tree = vec![Some(FrankenNode::LeafNode(franken_leaf(vec![extension])))];
}
tree.tls_serialize_detached().unwrap()
}
#[test]
fn builder_matches_frankenstein_wire_format() {
for depth in 0..=4 {
let hand = build_nested_ratchet_tree(depth);
let franken = build_nested_ratchet_tree_frankenstein(depth);
assert_eq!(
hand, franken,
"hand-built bytes must equal the Frankenstein wire encoding at depth {depth}"
);
}
}
#[test]
fn valid_ratchet_tree_still_decodes() {
let bytes = build_nested_ratchet_tree(0);
let decoded = RatchetTreeIn::tls_deserialize(&mut bytes.as_slice());
assert!(
decoded.is_ok(),
"a valid ratchet tree must still decode, got {decoded:?}"
);
}
#[test]
fn shallow_nesting_returns_err_not_panic() {
let bytes = build_nested_ratchet_tree(2);
let decoded = RatchetTreeIn::tls_deserialize(&mut bytes.as_slice());
assert!(
decoded.is_err(),
"a ratchet_tree extension is not valid inside a leaf node and must be \
rejected with Err, got Ok"
);
}
const CHILD_ENV: &str = "OPENMLS_NESTED_EXT_REPRO_CHILD";
const REPRO_DEPTH: usize = 100_000;
const REPRO_CHILD_STACK: usize = 256 * 1024;
#[test]
fn deeply_nested_ratchet_tree_does_not_overflow_the_stack() {
if std::env::var(CHILD_ENV).is_ok() {
run_decoder_child();
return;
}
let exe = std::env::current_exe().expect("current test executable");
let output = Command::new(&exe)
.args([
"--exact",
"deeply_nested_ratchet_tree_does_not_overflow_the_stack",
"--nocapture",
])
.env(CHILD_ENV, "1")
.output()
.expect("failed to spawn child test process");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("CHILD_SURVIVED"),
"the decoder did not return; it most likely overflowed the stack on the \
nested input. Child stderr:\n{stderr}"
);
assert!(
stderr.contains("CHILD_SURVIVED: decoder returned is_err=true"),
"the decoder accepted a nested ratchet_tree inside a leaf node. Child \
stderr:\n{stderr}"
);
assert!(
output.status.code().is_some(),
"expected the child to exit with a status code, but it was aborted \
without one; status was {:?}",
output.status
);
assert!(
output.status.signal().is_none(),
"expected the child to exit normally, but it was killed by a signal \
(stack-overflow abort); status was {:?}",
output.status
);
}
fn run_decoder_child() {
let depth: usize = std::env::var("OPENMLS_NESTED_EXT_REPRO_DEPTH")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(REPRO_DEPTH);
let stack: usize = std::env::var("OPENMLS_NESTED_EXT_REPRO_STACK")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(REPRO_CHILD_STACK);
let bytes = build_nested_ratchet_tree(depth);
eprintln!("child built {} bytes at depth {depth}", bytes.len());
let handle = std::thread::Builder::new()
.stack_size(stack)
.name("nested-extension-decoder".into())
.spawn(move || {
let result = RatchetTreeIn::tls_deserialize(&mut bytes.as_slice());
eprintln!(
"CHILD_SURVIVED: decoder returned is_err={}",
result.is_err()
);
})
.expect("failed to spawn decoder thread");
let _ = handle.join();
eprintln!("CHILD_COMPLETED_WITHOUT_ABORT");
}