recursion limit exceeded
主线位置:反序列化主线上的一道闸。每进一层
[/{,serde_json 的Deserializer都要经过Visitor再递归一次,栈跟着长;remaining_depth数到 0 就停。机制见 反序列化一侧 的「递归深度:最多 127 层」。
flowchart TB
open["读到数组或对象"] --> dec["remaining_depth 减一"]
dec --> zero{"等于 0 吗"}
zero -->|否| rec["递归读里面的值"]
zero -->|是| err["报递归超限"]
ign["IgnoredAny、未知字段"] --> iloop["循环跳过,不计数"]
off["disable_recursion_limit"] --> stack["只剩栈大小"]
check_recursion! 怎么加减、为什么从 128 起数却只能进 127 层、ignore_value 为什么不用栈、unbounded_depth 是哪个版本加的,都在 反序列化一侧 里跑过。这一页看它在真实类型上什么时候触发、怎么改。
本页实验在 M2 Max、macOS、rustc 1.99.0 上运行,debug 构建,主线程栈 8 MB(ulimit -s 是 8176)。依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154";关掉限制的实验在另一个 crate 里,serde_json 开了 unbounded_depth。
现象
127 层不是「127 层结构体」。src/bin/rl_repro.rs:
use serde::de::IgnoredAny;
use serde::{Deserialize, Serialize};
use serde_json::Value;
fn arrays(depth: usize) -> String {
format!("{}{}", "[".repeat(depth), "]".repeat(depth))
}
// A tree: every level is one JSON object plus one JSON array.
#[derive(Serialize, Deserialize, Debug)]
struct Node {
children: Vec<Node>,
}
fn chain(levels: usize) -> Node {
let mut node = Node { children: vec![] };
for _ in 1..levels {
node = Node { children: vec![node] };
}
node
}
fn main() {
for depth in [127, 128] {
let r = serde_json::from_str::<Value>(&arrays(depth)).map(|_| ()).map_err(|e| e.to_string());
println!("Value, {depth} arrays: {r:?}");
}
for levels in [63, 64] {
// serialization has no depth check
let text = serde_json::to_string(&chain(levels)).unwrap();
let r = serde_json::from_str::<Node>(&text).map(|_| ()).map_err(|e| e.to_string());
println!("Node, {levels} levels, {} brackets: to_string Ok, from_str {r:?}", text.matches(['[', '{']).count());
}
let e = serde_json::from_str::<Value>(&arrays(128)).unwrap_err();
println!("classify: {:?}", e.classify());
let r = serde_json::from_str::<IgnoredAny>(&arrays(100_000));
println!("IgnoredAny, 100000 arrays: {r:?}");
}
Value, 127 arrays: Ok(())
Value, 128 arrays: Err("recursion limit exceeded at line 1 column 128")
Node, 63 levels, 126 brackets: to_string Ok, from_str Ok(())
Node, 64 levels, 128 brackets: to_string Ok, from_str Err("recursion limit exceeded at line 1 column 832")
classify: Syntax
IgnoredAny, 100000 arrays: Ok(IgnoredAny)
- 计的是括号,不是 Rust 类型的层数。
Node每一层是一个对象{"children":加一个数组[,占两层,64 层的树就是 128 个括号,超限。一棵评论树、一个目录树、一个 AST,几十层就会碰到。 to_string两次都成功:序列化一侧不查深度,自己写出的 JSON 自己读不回来。- 错误的分类是
Category::Syntax,和「不是合法 JSON」归在一起。axum 的Json按classify()分流,Syntax走JsonSyntaxError,所以客户端看到的是 400 而不是 422(Json 提取器)。 - 不关心内容的部分(
IgnoredAny、结构体里的未知字段)再深都能跳过。
不经过文本的路径也不查深度。src/bin/rl_value.rs 在内存里搭一个 200 层的 Value:
use serde_json::Value;
fn main() {
let mut v = Value::Null;
for _ in 0..200 {
v = Value::Array(vec![v]);
}
let text = serde_json::to_string(&v).unwrap();
println!("from_str: {:?}", serde_json::from_str::<Value>(&text).map(|_| ()).map_err(|e| e.to_string()));
println!("from_value: {:?}", serde_json::from_value::<Value>(v).map(|_| ()).map_err(|e| e.to_string()));
}
from_str: Err("recursion limit exceeded at line 1 column 128")
from_value: Ok(())
所以「单元测试用 to_value / from_value 往返都过,上线后从请求体读就失败」是可能的。
出处
计数器是 Deserializer 的 remaining_depth: u8,Deserializer::new 把它设成 128。check_recursion! 套在每个 [ / { 分支外面:
if_checking_recursion_limit! {
$this.remaining_depth -= 1;
if $this.remaining_depth == 0 {
return Err($this.peek_error(ErrorCode::RecursionLimitExceeded));
}
}
ErrorCode::RecursionLimitExceeded 在 classify 里归为 Category::Syntax。整个 ser.rs 和 value/ 目录里没有任何深度计数,这就是上面「写得出、from_value 读得回、from_str 读不回」的原因。跳过未知内容的 ignore_value 是一个循环,不经过 check_recursion!。
问: 能不能把上限调到 256,而不是整个关掉?
答: 不能。1.0.154 里没有设置深度的 API:128 是 Deserializer::new 里写死的常数,字段类型是 u8,255 就是它能表示的上限。唯一的开关是 unbounded_depth feature 后面的 disable_recursion_limit,它把计数整个跳过(if_checking_recursion_limit! 里的代码不执行)。要「放宽但仍有上限」,得在 serde_json 外面做:限制请求体大小,或者在解析前自己数一遍括号。
修法
先看能不能让数据变浅。 深度来自数据结构本身时,最稳的办法是换一种表示:树写成扁平的节点列表加父节点编号,链表写成数组。这样 JSON 的深度是常数,任何格式、任何解析器都没有这个问题。
数据确实深、来源可信时,关掉限制,并给足栈。 Cargo.toml 里 serde_json = { version = "=1.0.154", features = ["unbounded_depth"] }。关掉之后递归下降就只受栈大小约束,disable_recursion_limit 的文档自己建议配合 serde_stacker 的动态增长栈(本机 registry 里没有这个 crate,这里不演示)。不加依赖的做法是在一个栈更大的线程里解析,src/bin/rl_unbounded.rs:
use serde::Deserialize;
use serde_json::Value;
fn arrays(depth: usize) -> String {
format!("{}{}", "[".repeat(depth), "]".repeat(depth))
}
// Drops a deep Value without recursing (Value's own Drop recurses).
fn drop_flat(value: Value) {
let mut stack = vec![value];
while let Some(value) = stack.pop() {
if let Value::Array(array) = value {
stack.extend(array);
}
}
}
fn parse_unbounded(json: &str) -> bool {
let mut de = serde_json::Deserializer::from_str(json);
de.disable_recursion_limit();
let value = Value::deserialize(&mut de);
let ok = value.is_ok();
if let Ok(v) = value {
drop_flat(v);
}
ok
}
fn main() {
let depth: usize = std::env::args().nth(1).unwrap().parse().unwrap();
let stack_mb: usize = std::env::args().nth(2).map(|s| s.parse().unwrap()).unwrap_or(0);
let json = arrays(depth);
if stack_mb == 0 {
println!("depth {depth}, main thread: parsed = {}", parse_unbounded(&json));
} else {
let handle = std::thread::Builder::new()
.stack_size(stack_mb << 20)
.spawn(move || parse_unbounded(&json))
.unwrap();
println!("depth {depth}, thread with {stack_mb} MiB stack: parsed = {}", handle.join().unwrap());
}
}
用 shell 循环依次传入 10000、10000 256、100000 256、1000000 256(== 和 exit= 两行是循环打印的):
== 10000
thread 'main' (2838196) has overflowed its stack
fatal runtime error: stack overflow, aborting
exit=134
== 10000 256
depth 10000, thread with 256 MiB stack: parsed = true
exit=0
== 100000 256
depth 100000, thread with 256 MiB stack: parsed = true
exit=0
== 1000000 256
thread '<unknown>' (2838272) has overflowed its stack
fatal runtime error: stack overflow, aborting
exit=134
- 8 MB 的主线程栈,一万层就栈溢出,进程以 SIGABRT(134)退出,连
Result都拿不到。这正是限制要防的情况。 - 256 MiB 的线程栈能读十万层,一百万层照样溢出。栈大只是把上限往后挪;输入不可信时,关掉限制就必须同时限制输入大小。
- 解析出来的
Value,它的Drop、Debug、Display也都是递归的(disable_recursion_limit的文档点名了这三个),所以程序里用drop_flat拆掉结果。序列化同样递归:src/bin/rl_ser_deep.rs在内存里循环搭一个 N 层的Value再to_string,1000 层输出to_string ok, 2004 bytes,100000 层在主线程上has overflowed its stack、exit=134。
来源不可信时,保留限制。 默认的 127 层就是为这种情况准备的:攻击者送来十万个 [,得到的是一个普通的 Err。真正用得上这么深结构的接口很少,把它当成 400 返回即可。
怎么确认没中招
- 数一数你的递归类型(树、AST、嵌套评论、
Box<Self>链)在 JSON 里每一层占几个括号:struct 是{,Vec/ 元组 / 序列是[,外部标签的 enum 变体再多一个{。层数乘以括号数不能到 128。 - 往返测试要经过文本:
to_string再from_str,用一个和线上一样深的样本。to_value/from_value不查深度,测不出来。 - 日志里看到 400 加
recursion limit exceeded,先确认是攻击流量还是合法数据变深了,再决定改表示还是关限制。
小结
- serde_json 读文本时最多嵌套 127 层,
[和{各算一层:每层是「对象 + 数组」的递归 struct,64 层就超限。 - 序列化、
to_value、from_value都不查深度:自己写出的 JSON 可能读不回来。 IgnoredAny和未知字段走ignore_value循环,不受限制。错误属于Category::Syntax,axum 返回 400。- 上限不可调,只能用
unbounded_depth的disable_recursion_limit整个关掉;关掉后深度只受栈约束(8 MB 主线程一万层就溢出),要配大栈线程或 serde_stacker,并限制输入大小。 - 首选的修法是让数据变浅:树写成节点列表加父编号。