Index: wp-includes/default-filters.php
===================================================================
--- wp-includes/default-filters.php	(revision 15513)
+++ wp-includes/default-filters.php	(working copy)
@@ -109,6 +109,10 @@
 	add_filter( $filter, 'convert_chars' );
 }
 
+// Pre save hierarchy
+add_filter( 'wp_insert_post_parent', 'wp_check_post_hierarchy_for_loops', 10, 2 );
+add_filter( 'wp_update_term_parent', 'wp_check_term_hierarchy_for_loops', 10, 3 );
+
 // Display filters
 add_filter( 'the_title', 'wptexturize'   );
 add_filter( 'the_title', 'convert_chars' );
Index: wp-includes/taxonomy.php
===================================================================
--- wp-includes/taxonomy.php	(revision 15513)
+++ wp-includes/taxonomy.php	(working copy)
@@ -1936,6 +1936,9 @@
 		}
 	}
 
+	// Check $parent to see if it will cause a hierarchy loop
+	$parent = apply_filters( 'wp_update_term_parent', $parent, $term_id, $taxonomy, compact( array_keys( $args ) ), $args );
+
 	// Check for duplicate slug
 	$id = $wpdb->get_var( $wpdb->prepare( "SELECT term_id FROM $wpdb->terms WHERE slug = %s", $slug ) );
 	if ( $id && ($id != $term_id) ) {
@@ -2682,3 +2685,94 @@
 
 	return false;
 }
+
+/**
+ * Checks the given subset of the term hierarchy for hierarchy loops.
+ * Prevents loops from forming and breaks those that it finds.
+ *
+ * Attached to the wp_update_term_parent filter.
+ *
+ * @since 3.1
+ * @uses WP_Term_Parent_Map
+ * @uses wp_find_hierarchy_loop()
+ *
+ * @param int $parent term_id of the parent for the term we're checking.
+ * @parem int $term_id The term we're checking.
+ *
+ * @return int The new parent for the term.
+ */
+function wp_check_term_hierarchy_for_loops( $parent, $term_id, $taxonomy ) {
+	// Nothing fancy here - bail
+	if ( !$parent )
+		return 0;
+
+	// Can't be its own parent
+	if ( $parent == $term_id )
+		return 0;
+
+	// Now look for larger loops
+
+	$term_parent_map = new WP_Term_Parent_Map( $taxonomy, array( $term_id => $parent ) );
+
+	if ( !$loop = wp_find_hierarchy_loop( $term_parent_map, $term_id ) )
+		return $parent; // No loop
+	
+	// Setting $parent to the given value causes a loop
+	if ( isset( $loop[$term_id] ) )
+		return 0;
+
+	// There's a loop, but it doesn't contain $term_id.  Break the loop.
+	foreach ( array_keys( $loop ) as $loop_member )
+		$term_parent_map[$loop_member] = 0;
+	return $parent;
+}
+
+/**
+ * Simulates array( $term_id => $parent, ... ) without having to load all terms.
+ *
+ * @link http://php.net/manual/class.arrayaccess.php
+ *
+ * @since 3.1
+ */
+class WP_Term_Parent_Map implements ArrayAccess {
+	public $taxonomy = '';
+	public $override = array();
+
+	/**
+	 * @param string $taxonomy
+	 * @param array $override A list of term_id => parent mappings used to override the results of get_term()->parent.
+	 *              Useful when checking for loops in hierarchies that are not yet stored in the DB.
+	 */
+	public function __construct( $taxonomy, $override = array() ) {
+		$this->taxonomy = $taxonomy;
+		$this->override = $override;
+	}
+
+	/* ArrayAccess */
+	public function offsetExists( $offset ) {
+		if ( isset( $this->override[$offset] ) )
+			return true;
+		$term = get_term( $offset, $this->taxonomy );
+		return $term && !is_wp_error( $term );
+	}
+	public function offsetGet( $offset ) {
+		if ( isset( $this->override[$offset] ) )
+			return $this->override[$offset];
+
+		$term = get_term( $offset, $this->taxonomy );
+		if ( !$term || is_wp_error( $term ) )
+			return;
+		return (int) $term->parent;
+	}
+	public function offsetSet( $offset, $value ) {
+		if ( isset( $this->override[$offset] ) )
+			$this->override[$offset] = $value;
+		else
+			wp_update_term( $offset, $this->taxonomy, array( 'parent' => $value ) );
+	}
+	public function offsetUnset( $offset ) {
+		if ( isset( $this->override[$offset] ) )
+			unset( $this->override[$offset] );
+		// else noop
+	}
+}
Index: wp-includes/post.php
===================================================================
--- wp-includes/post.php	(revision 15513)
+++ wp-includes/post.php	(working copy)
@@ -2273,17 +2273,8 @@
 	else
 		$post_parent = 0;
 
-	if ( !empty($post_ID) ) {
-		if ( $post_parent == $post_ID ) {
-			// Post can't be its own parent
-			$post_parent = 0;
-		} elseif ( !empty($post_parent) ) {
-			$parent_post = get_post($post_parent);
-			// Check for circular dependency
-			if ( isset( $parent_post->post_parent ) && $parent_post->post_parent == $post_ID )
-				$post_parent = 0;
-		}
-	}
+	// Check the post_parent to see if it will cause a hierarchy loop
+	$post_parent = apply_filters( 'wp_insert_post_parent', $post_parent, $post_ID, compact( array_keys( $postarr ) ), $postarr );
 
 	if ( isset($menu_order) )
 		$menu_order = (int) $menu_order;
@@ -4724,3 +4715,93 @@
 		add_filter('the_preview', '_set_preview');
 	}
 }
+
+/**
+ * Checks the given subset of the post hierarchy for hierarchy loops.
+ * Prevents loops from forming and breaks those that it finds.
+ *
+ * Attached to the wp_insert_post_parent filter.
+ *
+ * @since 3.1
+ * @uses WP_Post_Parent_Map
+ * @uses wp_find_hierarchy_loop()
+ *
+ * @param int $post_parent ID of the parent for the post we're checking.
+ * @parem int $post_ID ID of the post we're checking.
+ *
+ * @return int The new post_parent for the post.
+ */
+function wp_check_post_hierarchy_for_loops( $post_parent, $post_ID ) {
+	// Nothing fancy here - bail
+	if ( !$post_parent )
+		return 0;
+
+	// New post can't cause a loop
+	if ( empty( $post_ID ) )
+		return $post_parent;
+
+	// Can't be its own parent
+	if ( $post_parent == $post_ID )
+		return 0;
+
+	// Now look for larger loops
+
+	$post_parent_map = new WP_Post_Parent_Map( array( $post_ID => $post_parent ) );
+
+	if ( !$loop = wp_find_hierarchy_loop( $post_parent_map, $post_ID ) )
+		return $post_parent; // No loop
+
+	// Setting $post_parent to the given value causes a loop
+	if ( isset( $loop[$post_ID] ) )
+		return 0;
+
+	// There's a loop, but it doesn't contain $post_ID.  Break the loop.
+	foreach ( array_keys( $loop ) as $loop_member )
+		$post_parent_map[$loop_member] = 0;
+	return $post_parent;
+}
+
+/**
+ * Simulates array( $post_id => $post_parent, ... ) without having to load all posts.
+ *
+ * @link http://php.net/manual/class.arrayaccess.php
+ *
+ * @since 3.1
+ */
+class WP_Post_Parent_Map implements ArrayAccess {
+	public $override = array();
+
+	/**
+	 * @param array $override A list of post_ID => post_parent mappings used to override the results of get_post()->post_parent.
+	 *              Useful when checking for loops in hierarchies that are not yet stored in the DB.
+	 */
+	public function __construct( $override = array() ) {
+		$this->override = $override;
+	}
+
+	/* ArrayAccess */
+	public function offsetExists( $offset ) {
+		if ( isset( $this->override[$offset] ) )
+			return true;
+		return (bool) get_post( $offset );
+	}
+	public function offsetGet( $offset ) {
+		if ( isset( $this->override[$offset] ) )
+			return $this->override[$offset];
+
+		if ( !$post = get_post( $offset ) )
+			return;
+		return (int) $post->post_parent;
+	}
+	public function offsetSet( $offset, $value ) {
+		if ( isset( $this->override[$offset] ) )
+			$this->override[$offset] = $value;
+		else
+			wp_update_post( array( 'ID' => $offset, 'post_parent' => $value ) );
+	}
+	public function offsetUnset( $offset ) {
+		if ( isset( $this->override[$offset] ) )
+			unset( $this->override[$offset] );
+		// else noop
+	}
+}
Index: wp-includes/functions.php
===================================================================
--- wp-includes/functions.php	(revision 15513)
+++ wp-includes/functions.php	(working copy)
@@ -4297,4 +4297,59 @@
 	}
 }
 
-?>
+/**
+ * Finds hierarchy loops in an array that maps objects to parents.
+ *
+ * @since 3.1
+ *
+ * @param array $map array( ID => parent_ID, ... )
+ * @param $start The ID to start the loop check at
+ *
+ * @return array IDs of all members of loop
+ */
+function wp_find_hierarchy_loop( $map, $start ) {
+	if ( !$arbitrary_loop_member = wp_find_hierarchy_loop_tortoise_hare( $map, $start ) )
+		return array();
+
+	return wp_find_hierarchy_loop_tortoise_hare( $map, $arbitrary_loop_member, true );
+}
+
+/**
+ * Uses the "The Tortoise and the Hare" algorithm to detect loops.
+ *
+ * For every step of the algorithm, the hare takes two steps and the tortoise one.
+ * If the hare ever laps the tortoise, there must be a loop.
+ *
+ * @since 3.1
+ *
+ * @param array $map array( ID => parent_ID, ... )
+ * @param $start The ID to start the loop check at
+ * @param bool $_return_loop Return loop members or just detect presence of loop?
+ *             Only set to true if you already know the given $start is part of a loop
+ *             (otherwise the returned array might include branches)
+ *
+ * @return mixed scalar ID of some arbitrary member of the loop, or array of IDs of all members of loop if $_return_loop
+ */
+function wp_find_hierarchy_loop_tortoise_hare( $map, $start, $_return_loop = false ) {
+	$tortoise = $hare = $evanescent_hare = $start;
+	$return = array();
+
+	// Set evanescent_hare to one past hare
+	// Increment hare two steps
+	while ( $tortoise && ( $evanescent_hare = $map[$hare] ) && ( $hare = $map[$evanescent_hare] ) ) {
+		if ( $_return_loop )
+			$return[$tortoise] = $return[$evanescent_hare] = $return[$hare] = true;
+
+		// tortoise got lapped - must be a loop
+		if ( $tortoise == $evanescent_hare ) {
+			unset( $return[$hare] ); // $hare is not actually in the loop
+			return $_return_loop ? $return : $tortoise;
+		} else if ( $tortoise == $hare ) {
+			return $_return_loop ? $return : $tortoise;
+		}
+
+		$tortoise = $map[$tortoise]; // Increment tortoise by one step
+	}
+
+	return false;
+}
