Re: [PATCH RFC] xfs: hold buffer across unpin and potential shutdown processing

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Wed, Jun 02, 2021 at 09:32:34AM -0400, Brian Foster wrote:
> On Thu, May 06, 2021 at 03:29:50PM -0400, Brian Foster wrote:
> > On Thu, May 06, 2021 at 12:56:11PM +1000, Dave Chinner wrote:
> > > On Wed, May 05, 2021 at 07:50:17AM -0400, Brian Foster wrote:
> > > > On Tue, May 04, 2021 at 09:25:39AM +1000, Dave Chinner wrote:
> > > > > On Mon, May 03, 2021 at 08:18:16AM -0400, Brian Foster wrote:
> > > > > i.e. the problem here is that we've dropped the bip->bli_refcount
> > > > > before we've locked the buffer and taken a reference to it for
> > > > > the fail path?
> > > > > 
> > > > > OK, I see that xfs_buf_item_done() (called from ioend processing)
> > > > > simply frees the buf log item and doesn't care about the bli
> > > > > refcount at all. So the first ioend caller will free the buf log
> > > > > item regardless of whether there are other references to it at all.
> > > > > 
> > > > > IOWs, once we unpin the buffer, the bli attached to the buffer and
> > > > > being tracked in the AIL has -zero- references to the bli and so it
> > > > > gets freed unconditionally on IO completion.
> > > > > 
> > > > > That seems to the be the problem here - the bli is not reference
> > > > > counted while it is the AIL....
> > > > > 
> > > > 
> > > > I think it depends on how you look at it. As you point out, we've had
> > > > this odd bli reference count pattern for as long as I can remember where
> > > 
> > > Yes, and it's been a constant source of use-after free bugs in
> > > shutdown processing for as long as I can remember. I want to fix it
> > > so we don't have to keep band-aiding this code every time we change
> > > how some part of log item or stale inode/buffer processing works...
> > > 
> > 
> > IME, most of the bugs in this area tend to be shutdown/error related and
> > generally relate to the complexity of all the various states and
> > contexts for the different callback contexts. That isn't a direct result
> > of this bli refcount behavior, as odd as it is, though that certainly
> > contributes to the overall complexity.
> > 
> > > 
> > > > >  
> > > > > -	freed = atomic_dec_and_test(&bip->bli_refcount);
> > > > > -
> > > > > +	/*
> > > > > +	 * We can wake pin waiters safely now because we still hold the
> > > > > +	 * bli_refcount that was taken when the pin was gained.
> > > > > +	 */
> > > > >  	if (atomic_dec_and_test(&bp->b_pin_count))
> > > > >  		wake_up_all(&bp->b_waiters);
> > > > >  
> > > > > -	if (freed && stale) {
> > > > > -		ASSERT(bip->bli_flags & XFS_BLI_STALE);
> > > > > -		ASSERT(xfs_buf_islocked(bp));
> > > > > -		ASSERT(bp->b_flags & XBF_STALE);
> > > > > -		ASSERT(bip->__bli_format.blf_flags & XFS_BLF_CANCEL);
> > > > > -
> > > > > -		trace_xfs_buf_item_unpin_stale(bip);
> > > > > -
> > > > > -		if (remove) {
> > > > > -			/*
> > > > > -			 * If we are in a transaction context, we have to
> > > > > -			 * remove the log item from the transaction as we are
> > > > > -			 * about to release our reference to the buffer.  If we
> > > > > -			 * don't, the unlock that occurs later in
> > > > > -			 * xfs_trans_uncommit() will try to reference the
> > > > > -			 * buffer which we no longer have a hold on.
> > > > > -			 */
> > > > > -			if (!list_empty(&lip->li_trans))
> > > > > -				xfs_trans_del_item(lip);
> > > > > -
> > > > > -			/*
> > > > > -			 * Since the transaction no longer refers to the buffer,
> > > > > -			 * the buffer should no longer refer to the transaction.
> > > > > -			 */
> > > > > -			bp->b_transp = NULL;
> > > > > +	if (!stale) {
> > > > > +		if (!remove) {
> > > > > +			/* Nothing to do but drop the refcount the pin owned. */
> > > > > +			atomic_dec(&bip->bli_refcount);
> > > > > +			return;
> > > > >  		}
> > > > 
> > > > Hmm.. this seems a bit wonky to me. This code historically acts on the
> > > > drop of the final reference to the bli.
> > > 
> > > Yes, and that's the problem that needs fixing. The AIL needs a
> > > reference so that we aren't racing with writeback from the AIL to
> > > free the object because both sets of code run without actually
> > > holding an active reference to the BLI...
> > > 
> > > > This is not critical for the
> > > > common (!stale && !remove) case because that's basically a no-op here
> > > > outside of dropping the reference, and it looks like the stale buffer
> > > > handling code further down continues to follow that model, but in this
> > > > branch it seems we're trying to be clever in how the reference is
> > > > managed and as a result can act on a bli that might actually have
> > > > additional references.
> > > 
> > > Who cares? If something else has active references, then we must not
> > > free the bli or buffer here, anyway. The lack of active references
> > > by active BLI usres is why we keep getting use-after-free bugs in
> > > this code.....
> > > 
> > 
> > Well, this impacts more than just whether we free the buffer or not in
> > the abort case. This potentially runs the I/O failure sequence on a
> > pinned buffer, or blocks log I/O completion on a buffer lock that might
> > be held by a transaction.
> > 
> > I don't know if these are immediate problems or not and this is all
> > abort/shutdown related, but re: my point above around shutdown issues,
> > I'd prefer to try and avoid these kind of oddball quirks if we can vs.
> > just replace the historical quirks with new ones.
> > 
> > > > If so, I don't think it's appropriate to run
> > > > through the error sequence that follows.
> > > > 
> > > > >  
> > > > >  		/*
> > > > > -		 * If we get called here because of an IO error, we may or may
> > > > > -		 * not have the item on the AIL. xfs_trans_ail_delete() will
> > > > > -		 * take care of that situation. xfs_trans_ail_delete() drops
> > > > > -		 * the AIL lock.
> > > > > -		 */
> > > > > -		if (bip->bli_flags & XFS_BLI_STALE_INODE) {
> > > > > -			xfs_buf_item_done(bp);
> > > > > -			xfs_buf_inode_iodone(bp);
> > > > > -			ASSERT(list_empty(&bp->b_li_list));
> > > > > -		} else {
> > > > > -			xfs_trans_ail_delete(lip, SHUTDOWN_LOG_IO_ERROR);
> > > > > -			xfs_buf_item_relse(bp);
> > > > > -			ASSERT(bp->b_log_item == NULL);
> > > > > -		}
> > > > > -		xfs_buf_relse(bp);
> > > > > -	} else if (freed && remove) {
> > > > > -		/*
> > > > > +		 * Fail the IO before we drop the bli refcount. This guarantees
> > > > > +		 * that a racing writeback completion also failing the buffer
> > > > > +		 * and running completion will not remove the last reference to
> > > > > +		 * the bli and free it from under us.
> > > > > +		 *
> > > > >  		 * The buffer must be locked and held by the caller to simulate
> > > > >  		 * an async I/O failure.
> > > > >  		 */
> > > > > @@ -559,7 +555,62 @@ xfs_buf_item_unpin(
> > > > >  		xfs_buf_hold(bp);
> > > > >  		bp->b_flags |= XBF_ASYNC;
> > > > >  		xfs_buf_ioend_fail(bp);
> > > > > +		xfs_buf_item_relse(bp);
> > > > 
> > > > Did you mean for this to be xfs_buf_item_put() instead of _relse()? The
> > > 
> > > Yes. I did say "untested" which implies the patch isn't complete or will
> > > work. It's just a demonstration of how this reference counting might
> > > be done, not a complete, working solution. Patches are a much faster
> > > way of explaining the overall solution that plain text...
> > > 
> > 
> > Sure, I'm just trying to clarify intent. It's a refcount patch so
> > whether we drop a refcount or explicitly free an objects is particularly
> > relevant. ;)
> > 
> > > > >  	}
> > > > > +
> > > > > +	/*
> > > > > +	 * Stale buffer - only process it if this is the last reference to the
> > > > > +	 * BLI. If this is the last BLI reference, then the buffer will be
> > > > > +	 * locked and have two references - once from the transaction commit and
> > > > > +	 * one from the BLI - and we do not unlock and release transaction
> > > > > +	 * reference until we've finished cleaning up the BLI.
> > > > > +	 */
> > > > > +	if (!atomic_dec_and_test(&bip->bli_refcount))
> > > > > +		return;
> > > > > +
> > > > 
> > > > If the buffer is stale, will this ever be the last reference now that
> > > > _item_committed() bumps the refcount?
> > > 
> > > If the AIL has a reference, then no.
> > > 
> > 
> > Ok, but how would we get here without an AIL reference? That seems
> > impossible to me based on your patch.
> > 
> > > > This change also seems to have
> > > > ramifications for the code that follows, such as if a staled buffer is
> > > > already in the AIL (with a bli ref), would this code ever get to the
> > > > point of removing it?
> > > 
> > > That's easy enough to handle - if the buffer is stale and we are in
> > > this code, we hold the buffer locked. Hence we can remove from the
> > > AIL if it is in the AIL and drop that reference, too. Indeed, this
> > > code already does the AIL removal, so all this requires is a simple
> > > rearrangement of the logic in this function....
> > > 
> > 
> > Hmm, I don't think that's a correct assertion. If the buffer is stale
> > and we're in this code and we have the last reference to the bli, then
> > we know we hold the buffer locked. Otherwise, ISTM we can get here while
> > the transaction that staled the buffer might still own the lock.
> > 
> > > The only difference is that we have to do this before we drop the
> > > current reference we have on the BLI, which is not what we do now
> > > and that's where all the problems lie.
> > > 
> > > > All in all, I'll reiterate that I think it would be nice to fix up the
> > > > bli reference count handling in general, but I think the scope and
> > > > complexity of that work is significantly beyond what is reasonably
> > > > necessary to fix this bug.
> > > 
> > > And so leaving the underlying problem in the code for the next set
> > > of changes someone does to trigger the problem in a differen way.
> > > We've indentified what the root cause is, so can we please spend
> > > the time to fix it properly?
> > > 
> > 
> > That is not what I'm saying at all. I'm saying that I'd prefer to fix
> > the bug first and then step back and evaluate the overall refcount
> > design independently because the latter is quite complex and there are
> > all kinds of subtle interactions that the RFC patch just glazes over (by
> > design). For example, how log recovery processes bli's slightly
> > differently looks like yet another impedence mismatch from when the fs
> > is fully active.
> > 
> 
> Just an update on this particular bit...
> 
> I've probably spent about a week and a half now working through some
> attempts to rework the bli refcount handling (going back to the drawing
> board at least a couple times) and while I can get to something mostly
> functional (surviving an fstests run), the last steps to test/prove a
> solid/reliable implementation end up falling short. On extended testing
> I end up sorting through the same kind of shutdown/unmount hang, use
> after free, extremely subtle interactions that I've spent many hours
> trying to stamp out over the past several years.
> 
> Because of that, I don't think this is something worth pushing for in
> the short term. While the proposed idea sounds nice in theory, my take
> away in practice is that the current design is in place for a reason
> (i.e. the refcount historically looks like a transaction reference count
> used to provide unlocked/isolated access through the log subsystem as
> opposed to a traditional memory object lifecycle reference count) and
> has quite a lot of incremental stability that has been baked in over
> time.
> 
> I do still think this is (or should be :P) ultimately fixable, but I'm
> wondering if we're probably better served by exploring some of the
> historical warts and subtle rules/dependencies/hurdles of the current
> model to see if they can be removed or simplified to the point where the
> reference counting incrementally and naturally becomes a bit more
> straightforward. I probably need to reset my brain and think a little
> more about that...

What do you want to do in the meantime?

How about: Elevate this patch from RFC to regular patch status (perhaps
with an XXX comment outlining the gap as a breadcrumb to remind future
us?) and merge that so that we at least fix the immediate UAF problem?

I suspected that figuring out all the subtleties of the bli lifetimes
would be an intense effort.

--D

> 
> Brian
> 
> > So I propose to rework my patch a bit into something that reflects the
> > intended direction (see below for an untested diff) and proceed from
> > there...
> > 
> > Brian
> > 
> > --- 8< ---
> > 
> > diff --git a/fs/xfs/xfs_buf_item.c b/fs/xfs/xfs_buf_item.c
> > index fb69879e4b2b..7ff31788512b 100644
> > --- a/fs/xfs/xfs_buf_item.c
> > +++ b/fs/xfs/xfs_buf_item.c
> > @@ -475,17 +475,8 @@ xfs_buf_item_pin(
> >  }
> >  
> >  /*
> > - * This is called to unpin the buffer associated with the buf log
> > - * item which was previously pinned with a call to xfs_buf_item_pin().
> > - *
> > - * Also drop the reference to the buf item for the current transaction.
> > - * If the XFS_BLI_STALE flag is set and we are the last reference,
> > - * then free up the buf log item and unlock the buffer.
> > - *
> > - * If the remove flag is set we are called from uncommit in the
> > - * forced-shutdown path.  If that is true and the reference count on
> > - * the log item is going to drop to zero we need to free the item's
> > - * descriptor in the transaction.
> > + * This is called to unpin the buffer associated with the buf log item which
> > + * was previously pinned with a call to xfs_buf_item_pin().
> >   */
> >  STATIC void
> >  xfs_buf_item_unpin(
> > @@ -502,12 +493,26 @@ xfs_buf_item_unpin(
> >  
> >  	trace_xfs_buf_item_unpin(bip);
> >  
> > +	/*
> > +	 * Drop the bli ref associated with the pin and grab the hold required
> > +	 * for the I/O simulation failure in the abort case. We have to do this
> > +	 * before the pin count drops because the AIL doesn't acquire a bli
> > +	 * reference. Therefore if the refcount drops to zero, the bli could
> > +	 * still be AIL resident and the buffer submitted for I/O (and freed on
> > +	 * completion) at any point before we return. This can be removed once
> > +	 * the AIL properly holds a reference on the bli.
> > +	 */
> >  	freed = atomic_dec_and_test(&bip->bli_refcount);
> > -
> > +	if (freed && !stale && remove)
> > +		xfs_buf_hold(bp);
> >  	if (atomic_dec_and_test(&bp->b_pin_count))
> >  		wake_up_all(&bp->b_waiters);
> >  
> > -	if (freed && stale) {
> > +	 /* nothing to do but drop the pin count if the bli is active */
> > +	if (!freed)
> > +		return;
> > +
> > +	if (stale) {
> >  		ASSERT(bip->bli_flags & XFS_BLI_STALE);
> >  		ASSERT(xfs_buf_islocked(bp));
> >  		ASSERT(bp->b_flags & XBF_STALE);
> > @@ -550,13 +555,13 @@ xfs_buf_item_unpin(
> >  			ASSERT(bp->b_log_item == NULL);
> >  		}
> >  		xfs_buf_relse(bp);
> > -	} else if (freed && remove) {
> > +	} else if (remove) {
> >  		/*
> >  		 * The buffer must be locked and held by the caller to simulate
> > -		 * an async I/O failure.
> > +		 * an async I/O failure. We acquired the hold for this case
> > +		 * before the buffer was unpinned.
> >  		 */
> >  		xfs_buf_lock(bp);
> > -		xfs_buf_hold(bp);
> >  		bp->b_flags |= XBF_ASYNC;
> >  		xfs_buf_ioend_fail(bp);
> >  	}
> 



[Index of Archives]     [XFS Filesystem Development (older mail)]     [Linux Filesystem Development]     [Linux Audio Users]     [Yosemite Trails]     [Linux Kernel]     [Linux RAID]     [Linux SCSI]


  Powered by Linux