• James Chapman's avatar
    l2tp: fix possible UAF when cleaning up tunnels · f8ad00f3
    James Chapman authored
    syzbot reported a UAF caused by a race when the L2TP work queue closes a
    tunnel at the same time as a userspace thread closes a session in that
    tunnel.
    
    Tunnel cleanup is handled by a work queue which iterates through the
    sessions contained within a tunnel, and closes them in turn.
    
    Meanwhile, a userspace thread may arbitrarily close a session via
    either netlink command or by closing the pppox socket in the case of
    l2tp_ppp.
    
    The race condition may occur when l2tp_tunnel_closeall walks the list
    of sessions in the tunnel and deletes each one.  Currently this is
    implemented using list_for_each_safe, but because the list spinlock is
    dropped in the loop body it's possible for other threads to manipulate
    the list during list_for_each_safe's list walk.  This can lead to the
    list iterator being corrupted, leading to list_for_each_safe spinning.
    One sequence of events which may lead to this is as follows:
    
     * A tunnel is created, containing two sessions A and B.
     * A thread closes the tunnel, triggering tunnel cleanup via the work
       queue.
     * l2tp_tunnel_closeall runs in the context of the work queue.  It
       removes session A from the tunnel session list, then drops the list
       lock.  At this point the list_for_each_safe temporary variable is
       pointing to the other session on the list, which is session B, and
       the list can be manipulated by other threads since the list lock has
       been released.
     * Userspace closes session B, which removes the session from its parent
       tunnel via l2tp_session_delete.  Since l2tp_tunnel_closeall has
       released the tunnel list lock, l2tp_session_delete is able to call
       list_del_init on the session B list node.
     * Back on the work queue, l2tp_tunnel_closeall resumes execution and
       will now spin forever on the same list entry until the underlying
       session structure is freed, at which point UAF occurs.
    
    The solution is to iterate over the tunnel's session list using
    list_first_entry_not_null to avoid the possibility of the list
    iterator pointing at a list item which may be removed during the walk.
    
    Also, have l2tp_tunnel_closeall ref each session while it processes it
    to prevent another thread from freeing it.
    
    	cpu1				cpu2
    	---				---
    					pppol2tp_release()
    
    	spin_lock_bh(&tunnel->list_lock);
    	for (;;) {
    		session = list_first_entry_or_null(&tunnel->session_list,
    						   struct l2tp_session, list);
    		if (!session)
    			break;
    		list_del_init(&session->list);
    		spin_unlock_bh(&tunnel->list_lock);
    
     					l2tp_session_delete(session);
    
    		l2tp_session_delete(session);
    		spin_lock_bh(&tunnel->list_lock);
    	}
    	spin_unlock_bh(&tunnel->list_lock);
    
    Calling l2tp_session_delete on the same session twice isn't a problem
    per-se, but if cpu2 manages to destruct the socket and unref the
    session to zero before cpu1 progresses then it would lead to UAF.
    
    Reported-by: syzbot+b471b7c936301a59745b@syzkaller.appspotmail.com
    Reported-by: syzbot+c041b4ce3a6dfd1e63e2@syzkaller.appspotmail.com
    Fixes: d18d3f0a ("l2tp: replace hlist with simple list for per-tunnel session list")
    Signed-off-by: default avatarJames Chapman <jchapman@katalix.com>
    Signed-off-by: default avatarTom Parkin <tparkin@katalix.com>
    Link: https://patch.msgid.link/20240704152508.1923908-1-jchapman@katalix.comSigned-off-by: default avatarPaolo Abeni <pabeni@redhat.com>
    f8ad00f3
l2tp_core.c 47.7 KB