tech-kern archive

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

Re: rework rndsink and cprng locking



   Date: Tue, 18 Jun 2013 05:11:48 +0000
   From: Taylor R Campbell <riastradh%NetBSD.org@localhost>

   Some discussion with rmind@ today revealed a (highly unlikely) race
   that could cause a read from /dev/random to hang indefinitely even
   though entropy is available.  Attached is an updated patch that fixes
   it by adding a hook for rndsink callbacks to use under their internal
   locks to mark an rndsink ready for requeueing.

Oops...  Disregard that second patch -- it would cause other races in
destruction.  The attached kern_rndsink.c replacement fixes the race
in my original patch more easily, without requiring a hook for rndsink
callbacks to call.
/*      $NetBSD$        */

/*-
 * Copyright (c) 2013 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Taylor R. Campbell.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD$");

#include <sys/param.h>
#include <sys/types.h>
#include <sys/condvar.h>
#include <sys/kmem.h>
#include <sys/mutex.h>
#include <sys/queue.h>
#include <sys/rnd.h>
#include <sys/rndsink.h>

#include <dev/rnd_private.h>    /* XXX provisional, for rnd_extract_data */

struct rndsink {
        /* Callback state.  */
        enum {
                RNDSINK_IDLE,           /* no callback in progress */
                RNDSINK_QUEUED,         /* queued for callback */
                RNDSINK_IN_FLIGHT,      /* callback called */
                RNDSINK_REQUEUED,       /* queued again before callback done */
                RNDSINK_DEAD,           /* destroyed */
        }                       rsink_state;

        /* Entry on the queue of rndsinks, iff in the RNDSINK_QUEUED state.  */
        TAILQ_ENTRY(rndsink)    rsink_entry;

        /*
         * Notifies rndsink_destroy when rsink_state transitions to
         * RNDSINK_IDLE or RNDSINK_QUEUED.
         */
        kcondvar_t              rsink_cv;

        /* rndsink_create parameters.  */
        unsigned int            rsink_bytes;
        rndsink_callback_t      *rsink_callback;
        void                    *rsink_arg;
};

static kmutex_t                 rndsinks_lock __cacheline_aligned;
static TAILQ_HEAD(, rndsink)    rndsinks = TAILQ_HEAD_INITIALIZER(rndsinks);

void
rndsinks_init(void)
{

        /*
         * This mutex must be at an ipl as high as the highest ipl of
         * anyone who wants to call rndsink_request.
         *
         * XXX Call this IPL_RND, perhaps.
         */
        mutex_init(&rndsinks_lock, MUTEX_DEFAULT, IPL_VM);
}

/*
 * XXX Provisional -- rndpool_extract and rndpool_maybe_extract should
 * move into kern_rndpool.c.
 */
extern rndpool_t rnd_pool;
extern kmutex_t rndpool_mtx;

/*
 * Fill the buffer with as much entropy as we can.  Return true if it
 * has full entropy and false if not.
 */
static bool
rndpool_extract(void *buffer, size_t bytes)
{
        const size_t extracted = rnd_extract_data(buffer, bytes,
            RND_EXTRACT_GOOD);

        if (extracted < bytes) {
                (void)rnd_extract_data((uint8_t *)buffer + extracted,
                    bytes - extracted, RND_EXTRACT_ANY);
                mutex_spin_enter(&rndpool_mtx);
                rnd_getmore(bytes - extracted);
                mutex_spin_exit(&rndpool_mtx);
                return false;
        }

        return true;
}

/*
 * If we have as much entropy as is requested, fill the buffer with it
 * and return true.  Otherwise, leave the buffer alone and return
 * false.
 */
static bool
rndpool_maybe_extract(void *buffer, size_t bytes)
{
        bool ok;

        KASSERT(bytes <= RNDSINK_MAX_BYTES);
        CTASSERT(RND_ENTROPY_THRESHOLD <= 0xffffffffUL);
        CTASSERT(RNDSINK_MAX_BYTES <= (0xffffffffUL - RND_ENTROPY_THRESHOLD));
        CTASSERT((RNDSINK_MAX_BYTES + RND_ENTROPY_THRESHOLD) <=
            (0xffffffffUL / NBBY));

        const uint32_t bits_needed = ((bytes + RND_ENTROPY_THRESHOLD) * NBBY);

        mutex_spin_enter(&rndpool_mtx);
        if (bits_needed <= rndpool_get_entropy_count(&rnd_pool)) {
                const uint32_t extracted __unused =
                    rndpool_extract_data(&rnd_pool, buffer, bytes,
                        RND_EXTRACT_GOOD);

                KASSERT(extracted == bytes);

                ok = true;
        } else {
                ok = false;
                rnd_getmore(howmany(bits_needed -
                        rndpool_get_entropy_count(&rnd_pool), NBBY));
        }
        mutex_spin_exit(&rndpool_mtx);

        return ok;
}

void
rndsinks_distribute(void)
{
        uint8_t buffer[RNDSINK_MAX_BYTES];
        struct rndsink *rndsink;

        explicit_bzero(buffer, sizeof(buffer)); /* paranoia */

        mutex_spin_enter(&rndsinks_lock);
        while ((rndsink = TAILQ_FIRST(&rndsinks)) != NULL) {
                KASSERT(rndsink->rsink_state == RNDSINK_QUEUED);

                /* Bail if we can't get some entropy for this rndsink.  */
                if (!rndpool_maybe_extract(buffer, rndsink->rsink_bytes))
                        break;

                /*
                 * Got some entropy.  Take the sink off the queue and
                 * feed the entropy to the callback, with rndsinks_lock
                 * dropped.  While running the callback, lock out
                 * rndsink_destroy by marking the sink in flight.
                 */
                TAILQ_REMOVE(&rndsinks, rndsink, rsink_entry);
                rndsink->rsink_state = RNDSINK_IN_FLIGHT;
                mutex_spin_exit(&rndsinks_lock);

                (*rndsink->rsink_callback)(rndsink->rsink_arg, buffer,
                    rndsink->rsink_bytes);
                explicit_bzero(buffer, rndsink->rsink_bytes);

                mutex_spin_enter(&rndsinks_lock);

                /*
                 * If, while the callback was running, anyone requested
                 * it be queued up again, do so now.  Otherwise, idle.
                 * Either way, it is now safe to destroy, so wake the
                 * pending rndsink_destroy, if there is one.
                 */
                if (rndsink->rsink_state == RNDSINK_REQUEUED) {
                        TAILQ_INSERT_TAIL(&rndsinks, rndsink, rsink_entry);
                        rndsink->rsink_state = RNDSINK_QUEUED;
                } else {
                        KASSERT(rndsink->rsink_state == RNDSINK_IN_FLIGHT);
                        rndsink->rsink_state = RNDSINK_IDLE;
                }
                cv_broadcast(&rndsinks_lock);
        }
        mutex_spin_exit(&rndsinks_lock);

        explicit_bzero(buffer, sizeof(buffer)); /* paranoia */
}

static void
rndsinks_enqueue(struct rndsink *rndsink)
{

        KASSERT(mutex_owned(&rndsinks_lock));

        /* XXX Kick any on-demand entropy sources too.  */

        switch (rndsink->rsink_state) {
        case RNDSINK_IDLE:
                /* Not on the queue and nobody is handling it.  */
                TAILQ_INSERT_TAIL(&rndsinks, rndsink, rsink_entry);
                rndsink->rsink_state = RNDSINK_QUEUED;
                break;

        case RNDSINK_QUEUED:
                /* Already on the queue.  */
                break;

        case RNDSINK_IN_FLIGHT:
                /* Someone is handling it.  Ask to queue it up again.  */
                rndsink->rsink_state = RNDSINK_REQUEUED;
                break;

        case RNDSINK_REQUEUED:
                /* Already asked to queue it up again.  */
                break;

        case RNDSINK_DEAD:
                panic("requesting entropy from dead rndsink: %p", rndsink);

        default:
                panic("rndsink %p in unknown state: %d", rndsink,
                    (int)rndsink->rsink_state);
        }
}

struct rndsink *
rndsink_create(size_t bytes, rndsink_callback_t *callback, void *arg)
{
        struct rndsink *const rndsink = kmem_alloc(sizeof(*rndsink), KM_SLEEP);

        KASSERT(bytes <= RNDSINK_MAX_BYTES);

        rndsink->rsink_state = RNDSINK_IDLE;
        cv_init(&rndsink->rsink_cv, "rndsink");
        rndsink->rsink_bytes = bytes;
        rndsink->rsink_callback = callback;
        rndsink->rsink_arg = arg;

        return rndsink;
}

void
rndsink_destroy(struct rndsink *rndsink)
{

        /*
         * Make sure the rndsink is off the queue, and if it's already
         * in flight, wait for the callback to complete.
         */
        mutex_spin_enter(&rndsinks_lock);
        while (rndsink->rsink_state != RNDSINK_IDLE) {
                switch (rndsink->rsink_state) {
                case RNDSINK_QUEUED:
                        TAILQ_REMOVE(&rndsinks, rndsink, rsink_entry);
                        rndsink->rsink_state = RNDSINK_IDLE;
                        break;

                case RNDSINK_IN_FLIGHT:
                case RNDSINK_REQUEUED:
                        cv_wait(&rndsink->rsink_cv, &rndsinks_lock);
                        break;

                case RNDSINK_DEAD:
                        panic("destroying dead rndsink: %p", rndsink);

                default:
                        panic("rndsink %p in unknown state: %d", rndsink,
                            (int)rndsink->rsink_state);
                }
        }
        rndsink->rsink_state = RNDSINK_DEAD;
        mutex_spin_exit(&rndsinks_lock);

        cv_destroy(&rndsink->rsink_cv);

        kmem_free(rndsink, sizeof(*rndsink));
}

void
rndsink_schedule(struct rndsink *rndsink)
{

        /* Optimistically check without the lock whether we're queued.  */
        if ((rndsink->rsink_state != RNDSINK_QUEUED) &&
            (rndsink->rsink_state != RNDSINK_REQUEUED)) {
                mutex_spin_enter(&rndsinks_lock);
                rndsinks_enqueue(rndsink);
                mutex_spin_exit(&rndsinks_lock);
        }
}

bool
rndsink_request(struct rndsink *rndsink, void *buffer, size_t bytes)
{

        KASSERT(bytes == rndsink->rsink_bytes);

        mutex_spin_enter(&rndsinks_lock);
        const bool full_entropy = rndpool_extract(buffer, bytes);
        if (!full_entropy)
                rndsinks_enqueue(rndsink);
        mutex_spin_exit(&rndsinks_lock);

        return full_entropy;
}


Home | Main Index | Thread Index | Old Index