Merge pull request #9281 from lightcodegit/fix/execute-parse-depth

🤖 Agent: caps `parse_nodes` recursion at 128 where `execute … run` redirects to the root, so the 400-deep chain from #9252 fails to parse instead of overflowing the server thread; the 400-deep test would abort the binary without it. Chosen over #9320, which capped execution only. Nothing else found.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-04 18:54:32 +01:00
commit e0798a4224
2 files changed
+51 -3

No files matched your search

+31 -3
View File
@@ -747,6 +747,13 @@ impl Suggests {
}
}
/// The deepest node/redirect recursion `parse_nodes` accepts. `execute … run`
/// redirects back to the root, so a crafted deeply nested command would recurse
/// once per level and overflow the parsing thread's stack (measured at ~300 on
/// the server thread). Far above any real command chain; vanilla likewise caps
/// command nesting.
const MAX_PARSE_DEPTH: usize = 128;
/// The command tree (CommandDispatcher). Node 0 is the root.
#[derive(Clone, Debug)]
pub struct Dispatcher {
@@ -925,7 +932,20 @@ impl Dispatcher {
so_far: &Context,
reg: &Registries,
level: u8,
depth: usize,
) -> ParseResults {
if depth >= MAX_PARSE_DEPTH {
// A crafted command nested through `execute … run` (which redirects
// back to the root) recurses here per level; stop before it
// overflows the parsing thread's stack. The branch yields no result,
// so the whole command simply fails to parse.
return ParseResults {
context: so_far.clone(),
input: input.to_owned(),
cursor,
errors: Vec::new(),
};
}
let mut errors = Vec::new();
let mut potentials: Vec<ParseResults> = Vec::new();
let original = Reader {
@@ -961,8 +981,15 @@ impl Dispatcher {
reader.skip();
if let Some(target) = redirect {
let child_context = Context::new(target, reader.cursor);
let parse =
self.parse_nodes(target, input, reader.cursor, &child_context, reg, level);
let parse = self.parse_nodes(
target,
input,
reader.cursor,
&child_context,
reg,
level,
depth + 1,
);
context.child = Some(Box::new(parse.context));
return ParseResults {
context,
@@ -978,6 +1005,7 @@ impl Dispatcher {
&context,
reg,
level,
depth + 1,
));
} else {
potentials.push(ParseResults {
@@ -1007,7 +1035,7 @@ impl Dispatcher {
/// Parse starting at byte `start` (after a leading '/').
pub fn parse_at(&self, input: &str, start: usize, reg: &Registries) -> ParseResults {
let context = Context::new(0, start);
self.parse_nodes(0, input, start, &context, reg, reg.permission)
self.parse_nodes(0, input, start, &context, reg, reg.permission, 0)
}
/// CommandDispatcher.getCompletionSuggestions at `cursor`: brigadier
/// lists every child, whatever its requirement.
+20
View File
@@ -1085,3 +1085,23 @@ fn local_coordinate_missing_after_space_expects_a_coordinate() {
assert_eq!(parse("^1 ^2 "), ("argument.pos.missing.double".into(), 6));
assert_eq!(parse("^1 ~"), ("argument.pos.mixed".into(), 0));
}
/// `execute … run` redirects back to the root, so a crafted deeply nested chain
/// recurses `parse_nodes` once per level. It must be rejected by the depth guard,
/// never overflow the parsing thread's stack (a stack overflow would abort this
/// test binary rather than let it pass).
#[test]
fn deeply_nested_execute_run_is_rejected_not_overflowed() {
let mut deep = String::from("say hi");
for _ in 0..400 {
deep = format!("execute if entity @s run {deep}");
}
assert!(run(&deep).is_err());
}
/// The guard is high enough that real command chains still parse.
#[test]
fn shallow_execute_run_still_parses() {
assert!(run("execute if entity @s run say hi").is_ok());
assert!(run("execute as @a at @s run tp @s 0 0 0").is_ok());
}