Re: [PATCH 1/2] ext4: fix NULL pointer dereference when journal restart fails

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

 



On Tue, 12 May 2015, Jan Kara wrote:

> Date: Tue, 12 May 2015 14:24:30 +0200
> From: Jan Kara <jack@xxxxxxx>
> To: Lukas Czerner <lczerner@xxxxxxxxxx>
> Cc: tytso@xxxxxxx, linux-ext4@xxxxxxxxxxxxxxx
> Subject: Re: [PATCH 1/2] ext4: fix NULL pointer dereference when journal
>     restart fails
> 
> On Wed 06-05-15 10:05:13, Lukas Czerner wrote:
> > Currently when journal restart fails, we'll have the h_transaction of
> > the handle set to NULL to indicate that the handle has been effectively
> > aborted. However it's not _really_ aborted and we need to treat this
> > differently because an abort indicates critical error which might not be
> > the case here.
> > 
> > So we handle this situation quietly in the jbd2_journal_stop() and just
> > free the handle and exit because everything else has been done before we
> > attempted (and failed) to restart the journal.
> > 
> > Unfortunately there are a number of problems with that approach
> > introduced with commit
> > 
> > 41a5b913197c "jbd2: invalidate handle if jbd2_journal_restart()
> > fails"
> > 
> > First of all in ext4 jbd2_journal_stop() will be called through
> > __ext4_journal_stop() where we would try to get a hold of the superblock
> > by dereferencing h_transaction which in this case would lead to NULL
> > pointer dereference and crash.
> > 
> > In addition we're going to free the handle regardless of the refcount
> > which is bad as well, because other's up the call chain will still
> > reference the handle so we might potentially reference already freed
> > memory.
> > 
> > I think that setting the h_transaction to NULL is just a hack and we can
> > do this properly by introducing h_stopped flag to the handle structure.
>   No, it isn't a hack. It is actually clearly representing what has
> happened. By the time we set h_transaction to NULL, handle isn't associated
> with that transaction in any way (we decremented transaction->t_updates and
> thus the transaction can happily commit and be freed). So keeping
> h_transaction set is just hiding the real problem and possibly causing
> use-after-free issues.

It's funny you say that since the current solution which is "not a
hack" has caused both NULL pointer dereference and use-after-free
issues :)

It was simply not expected by the code to get a h_transaction unset
among other problems. But I do understand your point and that we may
be asking for problems in the future by leaving h_transaction set
even though the handle has been disconnected from the transaction
and hence can be eventually freed.

I'll rework it.

Thanks!
-Lukas

> 
> Now the problems you describe above are real. IMO we should treat failed
> jbd2_journal_restart() as a handle abort as close as possible - after all
> jbd2_journal_restart() can fail only in circumstances when we'd abort the
> handle anyway. Fixing jbd2_journal_stop() to handle refcount properly is
> easy enough. We could fix ext4_journal_stop() to just silently call
> jbd2_journal_stop() for handle that is already detached and thus avoid the
> NULL pointer dereference.
> 
> If we really wanted to avoid NULL h_transaction, the clean fix for that
> would IMHO be to replace jbd2_journal_restart() with jbd2_journal_stop()
> and jbd2_journal_start() and return new handle from jbd2_journal_restart().
> But that would need some benchmarking to verify it doesn't regress
> something and it would be a pain to propagate new handle out of all the
> callers of ext4_journal_restart().
> 
> 								Honza
> 
> > Now we use h_stopped flag to indicate that the handle has already
> > been stopped so there is nothing else to do in jbd2_journal_stop() other
> > than decrease the refcount, or free the handle structure (in case refcount
> > drops to zero) and exit which exactly fits our case. So if we fail to start
> > the handle in jbd2__journal_restart() instead of setting h_transaction to
> > NULL we set the h_stopped flag and let the jbd2_journal_stop() deal with
> > this.
> > 
> > This will also fix the NULL dereference because we no longer free the
> > h_transaction.
> > 
> > Moreover remove all the WARN_ON's when we're dealing with already
> > stopped handle. Again the situation is quite similar with the aborted
> > handle and it is possible to get an already stopped handle, in this case
> > we do not want to emit an backtrace.
> > 
> > Signed-off-by: Lukas Czerner <lczerner@xxxxxxxxxx>
> > ---
> >  fs/jbd2/transaction.c | 45 ++++++++++++++++++++++++++-------------------
> >  include/linux/jbd2.h  | 20 +++++++++++++++++++-
> >  2 files changed, 45 insertions(+), 20 deletions(-)
> > 
> > diff --git a/fs/jbd2/transaction.c b/fs/jbd2/transaction.c
> > index 5f09370..34bd0c5 100644
> > --- a/fs/jbd2/transaction.c
> > +++ b/fs/jbd2/transaction.c
> > @@ -361,6 +361,7 @@ repeat:
> >  	handle->h_transaction = transaction;
> >  	handle->h_requested_credits = blocks;
> >  	handle->h_start_jiffies = jiffies;
> > +	handle->h_stopped = 0;
> >  	atomic_inc(&transaction->t_updates);
> >  	atomic_inc(&transaction->t_handle_count);
> >  	jbd_debug(4, "Handle %p given %d credits (total %d, free %lu)\n",
> > @@ -551,8 +552,7 @@ int jbd2_journal_extend(handle_t *handle, int nblocks)
> >  	int result;
> >  	int wanted;
> >  
> > -	WARN_ON(!transaction);
> > -	if (is_handle_aborted(handle))
> > +	if (is_handle_aborted_or_stopped(handle))
> >  		return -EROFS;
> >  	journal = transaction->t_journal;
> >  
> > @@ -627,11 +627,10 @@ int jbd2__journal_restart(handle_t *handle, int nblocks, gfp_t gfp_mask)
> >  	tid_t		tid;
> >  	int		need_to_start, ret;
> >  
> > -	WARN_ON(!transaction);
> >  	/* If we've had an abort of any type, don't even think about
> >  	 * actually doing the restart! */
> > -	if (is_handle_aborted(handle))
> > -		return 0;
> > +	if (is_handle_aborted_or_stopped(handle))
> > +		return -EROFS;
> >  	journal = transaction->t_journal;
> >  
> >  	/*
> > @@ -653,7 +652,7 @@ int jbd2__journal_restart(handle_t *handle, int nblocks, gfp_t gfp_mask)
> >  		wake_up(&journal->j_wait_updates);
> >  	tid = transaction->t_tid;
> >  	spin_unlock(&transaction->t_handle_lock);
> > -	handle->h_transaction = NULL;
> > +	jbd2_journal_stop_handle(handle);
> >  	current->journal_info = NULL;
> >  
> >  	jbd_debug(2, "restarting handle %p\n", handle);
> > @@ -785,8 +784,7 @@ do_get_write_access(handle_t *handle, struct journal_head *jh,
> >  	int need_copy = 0;
> >  	unsigned long start_lock, time_lock;
> >  
> > -	WARN_ON(!transaction);
> > -	if (is_handle_aborted(handle))
> > +	if (is_handle_aborted_or_stopped(handle))
> >  		return -EROFS;
> >  	journal = transaction->t_journal;
> >  
> > @@ -849,7 +847,7 @@ repeat:
> >  	unlock_buffer(bh);
> >  
> >  	error = -EROFS;
> > -	if (is_handle_aborted(handle)) {
> > +	if (is_handle_aborted_or_stopped(handle)) {
> >  		jbd_unlock_bh_state(bh);
> >  		goto out;
> >  	}
> > @@ -1051,9 +1049,8 @@ int jbd2_journal_get_create_access(handle_t *handle, struct buffer_head *bh)
> >  	int err;
> >  
> >  	jbd_debug(5, "journal_head %p\n", jh);
> > -	WARN_ON(!transaction);
> >  	err = -EROFS;
> > -	if (is_handle_aborted(handle))
> > +	if (is_handle_aborted_or_stopped(handle))
> >  		goto out;
> >  	journal = transaction->t_journal;
> >  	err = 0;
> > @@ -1266,8 +1263,7 @@ int jbd2_journal_dirty_metadata(handle_t *handle, struct buffer_head *bh)
> >  	struct journal_head *jh;
> >  	int ret = 0;
> >  
> > -	WARN_ON(!transaction);
> > -	if (is_handle_aborted(handle))
> > +	if (is_handle_aborted_or_stopped(handle))
> >  		return -EROFS;
> >  	journal = transaction->t_journal;
> >  	jh = jbd2_journal_grab_journal_head(bh);
> > @@ -1397,8 +1393,7 @@ int jbd2_journal_forget (handle_t *handle, struct buffer_head *bh)
> >  	int err = 0;
> >  	int was_modified = 0;
> >  
> > -	WARN_ON(!transaction);
> > -	if (is_handle_aborted(handle))
> > +	if (is_handle_aborted_or_stopped(handle))
> >  		return -EROFS;
> >  	journal = transaction->t_journal;
> >  
> > @@ -1530,8 +1525,19 @@ int jbd2_journal_stop(handle_t *handle)
> >  	tid_t tid;
> >  	pid_t pid;
> >  
> > -	if (!transaction)
> > -		goto free_and_exit;
> > +	if (is_handle_stopped(handle)) {
> > +		/*
> > +		 * Handle is stopped so there is nothing to do other than
> > +		 * decrease a refcount, or free the handle if refcount
> > +		 * drops to zero
> > +		 */
> > +		if (--handle->h_ref > 0) {
> > +			jbd_debug(4, "h_ref %d -> %d\n", handle->h_ref + 1,
> > +							 handle->h_ref);
> > +			return err;
> > +		} else
> > +			goto free_and_exit;
> > +	}
> >  	journal = transaction->t_journal;
> >  
> >  	J_ASSERT(journal_current_handle() == handle);
> > @@ -1665,7 +1671,9 @@ int jbd2_journal_stop(handle_t *handle)
> >  
> >  	if (handle->h_rsv_handle)
> >  		jbd2_journal_free_reserved(handle->h_rsv_handle);
> > +
> >  free_and_exit:
> > +	jbd2_journal_stop_handle(handle);
> >  	jbd2_free_handle(handle);
> >  	return err;
> >  }
> > @@ -2373,8 +2381,7 @@ int jbd2_journal_file_inode(handle_t *handle, struct jbd2_inode *jinode)
> >  	transaction_t *transaction = handle->h_transaction;
> >  	journal_t *journal;
> >  
> > -	WARN_ON(!transaction);
> > -	if (is_handle_aborted(handle))
> > +	if (is_handle_aborted_or_stopped(handle))
> >  		return -EROFS;
> >  	journal = transaction->t_journal;
> >  
> > diff --git a/include/linux/jbd2.h b/include/linux/jbd2.h
> > index 20e7f78..349975e 100644
> > --- a/include/linux/jbd2.h
> > +++ b/include/linux/jbd2.h
> > @@ -441,6 +441,7 @@ struct jbd2_journal_handle
> >  	unsigned int	h_jdata:	1;	/* force data journaling */
> >  	unsigned int	h_reserved:	1;	/* handle with reserved credits */
> >  	unsigned int	h_aborted:	1;	/* fatal error on handle */
> > +	unsigned int	h_stopped:	1;	/* handle is already stopped */
> >  	unsigned int	h_type:		8;	/* for handle statistics */
> >  	unsigned int	h_line_no:	16;	/* for handle statistics */
> >  
> > @@ -1266,18 +1267,35 @@ static inline int is_journal_aborted(journal_t *journal)
> >  	return journal->j_flags & JBD2_ABORT;
> >  }
> >  
> > +static inline int is_handle_stopped(handle_t *handle)
> > +{
> > +	return handle->h_stopped;
> > +}
> > +
> >  static inline int is_handle_aborted(handle_t *handle)
> >  {
> > -	if (handle->h_aborted || !handle->h_transaction)
> > +	if (handle->h_aborted)
> >  		return 1;
> >  	return is_journal_aborted(handle->h_transaction->t_journal);
> >  }
> >  
> > +static inline int is_handle_aborted_or_stopped(handle_t *handle)
> > +{
> > +	if (is_handle_aborted(handle) || is_handle_stopped(handle))
> > +		return 1;
> > +	return 0;
> > +}
> > +
> >  static inline void jbd2_journal_abort_handle(handle_t *handle)
> >  {
> >  	handle->h_aborted = 1;
> >  }
> >  
> > +static inline void jbd2_journal_stop_handle(handle_t *handle)
> > +{
> > +	handle->h_stopped = 1;
> > +}
> > +
> >  #endif /* __KERNEL__   */
> >  
> >  /* Comparison functions for transaction IDs: perform comparisons using
> > -- 
> > 1.8.3.1
> > 
> > --
> > To unsubscribe from this list: send the line "unsubscribe linux-ext4" in
> > the body of a message to majordomo@xxxxxxxxxxxxxxx
> > More majordomo info at  http://vger.kernel.org/majordomo-info.html
> 
--
To unsubscribe from this list: send the line "unsubscribe linux-ext4" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [Reiser Filesystem Development]     [Ceph FS]     [Kernel Newbies]     [Security]     [Netfilter]     [Bugtraq]     [Linux FS]     [Yosemite National Park]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Samba]     [Device Mapper]     [Linux Media]

  Powered by Linux