[PATCH 2/2] fork: plug a use after free of the returned process pointer

Mateusz Guzik mjguzik at gmail.com
Tue Feb 2 21:44:33 UTC 2016


On Tue, Feb 02, 2016 at 08:16:35PM +0200, Konstantin Belousov wrote:
> On Tue, Feb 02, 2016 at 06:56:52PM +0100, Mateusz Guzik wrote:
> > On Tue, Feb 02, 2016 at 03:23:22PM +0200, Konstantin Belousov wrote:
> > > On Tue, Feb 02, 2016 at 05:07:49AM +0100, Mateusz Guzik wrote:
> > > > +	flags = fr->fr_flags;
> > > Why not use fr->fr_flags directly ?  It is slightly more churn, but IMO
> > > it is worth it.
> > > 
> > 
> > I'm indiffernet on this one, can change it no problem.
> > 
> > > > +	/*
> > > > +	 * Hold the process so that it cannot exit after we make it runnable,
> > > > +	 * but before we wait for the debugger.
> > > Is this possible ?  The forked child must execute through fork_return(),
> > > and there we do ptracestop() before the child has a chance to ever return
> > > to usermode.
> > > 
> > > Do you mean a scenario where the debugger detaches before child executes
> > > fork_return() and TDP_STOPATFORK is cleared in advance ?
> > > 
> > 
> > The comment is somewhat misworded and I forgot to update it, how about
> > just stating we hold the process so that we can mark the thread runnable
> > and not have it disappear under we are done.
> This means that the reader has to guess too much, IMHO.
> 
> At least, add a note that despite fork_return() stops when the child
> is traced, it is not enough because ... .
> > 
> > While I have not tested this particular bug, prior to the patch the
> > following should possible: p2 is untraced and td2 is marked as runnable,
> > after which it exits and p2 is automatically reaped. If the code reaches
> > the TDB_STOPATFORK check after that, PROC_LOCK(p2) succeeds due to
> I.e. td2 is reused and the TDB_STOPATFORK is set by unrelated activity ?
> You reference the do_fork() code checking TDB_STOPATFORK, and not
> fork_return(), I guess.
> 
> 
> > processes being type stable. td2 dereference can cause no issues due to
> > threads being type stable as well. But the thread could have been resued
> > in a traced process, thus inducing cv_wait(&p2->p_dbgwait, ..) even
> > though td2 is not linked in p2 anymore and p2 is not even a valid
> > process, making curthread wait indefinitely since there is nobody to
> > wake it up.
> > 
> Well, if TDP_STOPATFORK bit is set, it has the same meaning due to type
> stability, and eventually the wake up would be performed.  It just the
> unintended sleep waiting for condvar which is problematic and which I
> agree with.


CPU0 is executing fork1. p2 is not traced.

CPU0				CPU1
p2 and td2 created
td2 is marked runnable
				td2 is scheduled here
				td2 does not have TDB_STOPATFORK set
				td2 exits
				p2 is autoreaped
				td2's space is reused
				td2 gets linked into p3
				td2 gets TDB_STOPATFORK
PROC_LOCK(p2);
TDB_STOPATFORK test on td2
cv_wait(&p2->p_dbgwait, ..);

So at this point p2 has no linked threads and is free. td2 belongs to
p3 and p2 is waiting for a wakeup which can't happen.

Now that I look at it this may be broken in an additonal way, which is
not fixed by the patch: what if td2 spawns a new thread and thr_exits?
In this case testing td2 is still invalid.  Maybe I'm just getting
paranoid here, I don't have time to properly test this right now. In
worst case should be fixable well enough with FIRST_THREAD_IN_PROC.

How about the following comment around _PHOLD:
We are going to make the main thread runnable. It can quickly exit,
causing the process to be reaped and possibly reused, thus invalidating
our p2 pointer. Protect against this by holding the process, which
postpones the exit.

And if the suspicion gets confimed the following would be added:
diff --git a/sys/kern/kern_fork.c b/sys/kern/kern_fork.c
index d0c3837..2a076ed 100644
--- a/sys/kern/kern_fork.c
+++ b/sys/kern/kern_fork.c
@@ -773,6 +773,12 @@ do_fork(struct thread *td, struct fork_req *fr, struct proc *p2, struct thread *
 
        PROC_LOCK(p2);
        /*
+        * By the time we got here the thread could have created a new thread
+        * and exited. Reload the main thread to ensure we got the right
+        * pointer.
+        */
+       td2 = FIRST_THREAD_IN_PROC(p2);
+       /*
         * Wait until debugger is attached to child.
         */
        while ((td2->td_dbgflags & TDB_STOPATFORK) != 0)


-- 
Mateusz Guzik <mjguzik gmail.com>


More information about the freebsd-hackers mailing list