diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs
index 326e7f741..b4d331255 100644
--- a/crates/xfa/src/layout.rs
+++ b/crates/xfa/src/layout.rs
@@ -769,6 +769,9 @@ impl Layouter<'_> {
self.warn_truncated(node, &ctx.som, instances);
let ctx_parent = ctx;
for i in 0..instances {
+ // Every instance spends budget, so nested repeats that place nothing still stop.
+ self.measures.set(self.measures.get().saturating_add(1));
+ self.check_measures()?;
let ctx = ctx_parent.with_som(&sf.common, sib + i);
let (rect, _, _) = self.cursor();
let (x, w) = avail.unwrap_or((rect.x, rect.w));
diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs
index 84c9fc00f..c5de7ad88 100644
--- a/crates/xfa/src/tests.rs
+++ b/crates/xfa/src/tests.rs
@@ -404,6 +404,18 @@ fn repeated_pictures_are_bounded_in_total_bytes() {
assert!(layout_xml(&ok).is_ok(), "one small picture must lay out");
}
+#[test]
+fn empty_repeating_subforms_spend_the_layout_budget() {
+ // Four nested levels of 50 empty instances are 6.25 million instances that place nothing
+ // and measure nothing; each one counts against the layout budget.
+ let level = "";
+ let tpl = format!("{}{}", level.repeat(4), "".repeat(4));
+ assert!(matches!(layout_xml(&tpl), Err(XfaError::TooLarge(_))));
+ // Three levels (125,000 instances) still lay out.
+ let tpl = format!("{}{}", level.repeat(3), "".repeat(3));
+ assert!(layout_xml(&tpl).is_ok());
+}
+
#[test]
fn the_packets_can_be_one_stream_or_named_parts() {
// Named parts, with the template split from the rest.