⬢ DragonFlyBSD Kernel Audit
DF-2584 / fix.diff
← back to finding ↓ download raw
--- a/sys/vfs/hammer2/hammer2_flush.c
+++ b/sys/vfs/hammer2/hammer2_flush.c
@@ -53,8 +53,28 @@
 
 #include "hammer2.h"
 
-#define HAMMER2_FLUSH_DEPTH_LIMIT	60      /* stack recursion limit */
+/*
+ * Recursion depth limit.  This MUST be comfortably below the depth at
+ * which hammer2_flush_core()/hammer2_flush_recurse() recursion exhausts
+ * a kernel thread stack.  Empirically (x86_64, INVARIANTS build), the
+ * stack dies between 35 and 40 recursion levels, so 60 was never safe.
+ * Chains at the limit are deferred (see hammer2_flush_defer) and
+ * re-driven with the depth reset to 0.
+ */
+#define HAMMER2_FLUSH_DEPTH_LIMIT	16      /* stack recursion limit */
 
+/*
+ * Deferred flush element.  When the recursion depth limit is reached the
+ * current chain is placed on this list (with a reference) and re-driven
+ * with the depth reset to 0 by hammer2_flush() after the stack has been
+ * popped.  This is the re-execution deferral the header comment describes.
+ */
+struct hammer2_flush_defer {
+	TAILQ_ENTRY(hammer2_flush_defer) entry;
+	hammer2_chain_t	*chain;
+};
+
+TAILQ_HEAD(hammer2_flush_defer_list, hammer2_flush_defer);
 
 /*
  * Recursively flush the specified chain.  The chain is locked and
@@ -66,6 +86,7 @@
 struct hammer2_flush_info {
 	hammer2_chain_t *parent;
 	int		depth;
+	struct	hammer2_flush_defer_list defer_list;
 	int		error;			/* cumulative error */
 	int		flags;
 #ifdef HAMMER2_SCAN_DEBUG
@@ -361,6 +382,8 @@
 hammer2_flush(hammer2_chain_t *chain, int flags)
 {
 	hammer2_flush_info_t info;
+	struct hammer2_flush_defer *delm;
+	hammer2_chain_t *dchain;
 	int loops;
 
 	/*
@@ -372,6 +395,7 @@
 	 * for re-execution after the stack has been popped.
 	 */
 	bzero(&info, sizeof(info));
+	TAILQ_INIT(&info.defer_list);
 	info.flags = flags & ~HAMMER2_FLUSH_TOP;
 
 	/*
@@ -414,6 +438,31 @@
 				Debugger("hell4");
 		}
 	}
+
+	/*
+	 * Process deferred chains.  Each is re-driven with the recursion
+	 * depth reset to 0, so arbitrarily deep topologies flush with a
+	 * bounded kernel stack.  Processing a deferred chain can queue
+	 * additional deferrals; keep going until the list is empty.
+	 */
+	while ((delm = TAILQ_FIRST(&info.defer_list)) != NULL) {
+		TAILQ_REMOVE(&info.defer_list, delm, entry);
+		dchain = delm->chain;
+		kfree(delm, M_HAMMER2);
+
+		if (info.parent != dchain->parent) {
+			if (info.parent)
+				hammer2_chain_drop(info.parent);
+			info.parent = dchain->parent;
+			if (info.parent)
+				hammer2_chain_ref(info.parent);
+		}
+		hammer2_chain_lock(dchain, HAMMER2_RESOLVE_MAYBE);
+		hammer2_flush_core(&info, dchain, info.flags);
+		hammer2_chain_unlock(dchain);
+		hammer2_chain_drop(dchain);		/* delm reference */
+	}
+
 #ifdef HAMMER2_SCAN_DEBUG
 	if (info.scan_count >= 10)
 	kprintf("hammer2_flush: scan_count %ld (%ld,%ld,%ld,%ld) "
@@ -484,6 +533,7 @@
 hammer2_flush_core(hammer2_flush_info_t *info, hammer2_chain_t *chain,
 		   int flags)
 {
+	struct hammer2_flush_defer *delm;
 	hammer2_chain_t *parent;
 	hammer2_dev_t *hmp;
 	int save_error;
@@ -579,9 +629,27 @@
 #endif
 	} else if (info->depth == HAMMER2_FLUSH_DEPTH_LIMIT) {
 		/*
-		 * Recursion depth reached.
+		 * Recursion depth reached.  Defer the chain; it keeps its
+		 * flush flags and is re-driven by hammer2_flush() with the
+		 * depth reset to 0 once this recursion has unwound.  Do
+		 * NOT panic here: a crafted filesystem image can drive the
+		 * topology arbitrarily deep.  Also do not recurse further;
+		 * the kernel stack is nearly exhausted by the time we get
+		 * here.
 		 */
-		panic("hammer2: flush depth limit");
+		delm = kmalloc(sizeof(*delm), M_HAMMER2, M_WAITOK | M_ZERO);
+		delm->chain = chain;
+		hammer2_chain_ref(chain);
+		TAILQ_INSERT_TAIL(&info->defer_list, delm, entry);
+		/*
+		 * Do NOT setflush() the parent here: re-flagging the
+		 * ancestor path makes flush_core() treat the subtree as
+		 * new work and rescan it, re-deferring over and over (the
+		 * deferred chain keeps its own ONFLUSH/MODIFIED/UPDATE
+		 * flags, and hammer2_flush() re-drives it from its
+		 * deferral list).
+		 */
+		goto done;
 	} else if (chain->flags & (HAMMER2_CHAIN_ONFLUSH |
 				   HAMMER2_CHAIN_DESTROY)) {
 		/*