dbus/dbus/dbus-mainloop.c
David Zeuthen 8703514397 Avoid using monotonic time in the DBUS_COOKIE_SHA1 authentication method
When libdbus-1 moved to using monotonic time support for the
DBUS_COOKIE_SHA1 authentication was broken, in particular
interoperability with non-libdbus-1 implementations such as GDBus.

The problem is that if monotonic clocks are available in the OS,
_dbus_get_current_time() will not return the number of seconds since
the Epoch so using it for DBUS_COOKIE_SHA1 will violate the D-Bus
specification. If both peers are using libdbus-1 it's not a problem
since both ends will use the wrong time and thus agree. However, if
the other end is another implementation and following the spec it will
not work.

First, we change _dbus_get_current_time() back so it always returns
time since the Epoch and we then rename it _dbus_get_real_time() to
make this clear. We then introduce _dbus_get_monotonic_time() and
carefully make all current users of _dbus_get_current_time() use it,
if applicable. During this audit, one of the callers,
_dbus_generate_uuid(), was currently using monotonic time but it was
decided to make it use real time instead.

Signed-off-by: David Zeuthen <davidz@redhat.com>
Reviewed-by: Simon McVittie <simon.mcvittie@collabora.co.uk>
Bug: https://bugs.freedesktop.org/show_bug.cgi?id=48580
2012-04-12 11:18:07 -04:00

940 lines
23 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/* dbus-mainloop.c Main loop utility
*
* Copyright © 2003, 2004 Red Hat, Inc.
* Copyright © 2011 Nokia Corporation
*
* Licensed under the Academic Free License version 2.1
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include <config.h>
#include "dbus-mainloop.h"
#ifndef DOXYGEN_SHOULD_SKIP_THIS
#include <dbus/dbus-hash.h>
#include <dbus/dbus-list.h>
#include <dbus/dbus-socket-set.h>
#include <dbus/dbus-watch.h>
#define MAINLOOP_SPEW 0
#if MAINLOOP_SPEW
#ifdef DBUS_ENABLE_VERBOSE_MODE
static const char*
watch_flags_to_string (int flags)
{
const char *watch_type;
if ((flags & DBUS_WATCH_READABLE) &&
(flags & DBUS_WATCH_WRITABLE))
watch_type = "readwrite";
else if (flags & DBUS_WATCH_READABLE)
watch_type = "read";
else if (flags & DBUS_WATCH_WRITABLE)
watch_type = "write";
else
watch_type = "not read or write";
return watch_type;
}
#endif /* DBUS_ENABLE_VERBOSE_MODE */
#endif /* MAINLOOP_SPEW */
struct DBusLoop
{
int refcount;
/** fd => dbus_malloc'd DBusList ** of references to DBusWatch */
DBusHashTable *watches;
DBusSocketSet *socket_set;
DBusList *timeouts;
int callback_list_serial;
int watch_count;
int timeout_count;
int depth; /**< number of recursive runs */
DBusList *need_dispatch;
/** TRUE if we will skip a watch next time because it was OOM; becomes
* FALSE between polling, and dealing with the results of the poll */
unsigned oom_watch_pending : 1;
};
typedef struct
{
DBusTimeout *timeout;
unsigned long last_tv_sec;
unsigned long last_tv_usec;
} TimeoutCallback;
#define TIMEOUT_CALLBACK(callback) ((TimeoutCallback*)callback)
static TimeoutCallback*
timeout_callback_new (DBusTimeout *timeout)
{
TimeoutCallback *cb;
cb = dbus_new (TimeoutCallback, 1);
if (cb == NULL)
return NULL;
cb->timeout = timeout;
_dbus_get_monotonic_time (&cb->last_tv_sec,
&cb->last_tv_usec);
return cb;
}
static void
timeout_callback_free (TimeoutCallback *cb)
{
dbus_free (cb);
}
static void
free_watch_table_entry (void *data)
{
DBusList **watches = data;
DBusWatch *watch;
/* DBusHashTable sometimes calls free_function(NULL) even if you never
* have NULL as a value */
if (watches == NULL)
return;
for (watch = _dbus_list_pop_first (watches);
watch != NULL;
watch = _dbus_list_pop_first (watches))
{
_dbus_watch_unref (watch);
}
_dbus_assert (*watches == NULL);
dbus_free (watches);
}
DBusLoop*
_dbus_loop_new (void)
{
DBusLoop *loop;
loop = dbus_new0 (DBusLoop, 1);
if (loop == NULL)
return NULL;
loop->watches = _dbus_hash_table_new (DBUS_HASH_INT, NULL,
free_watch_table_entry);
loop->socket_set = _dbus_socket_set_new (0);
if (loop->watches == NULL || loop->socket_set == NULL)
{
if (loop->watches != NULL)
_dbus_hash_table_unref (loop->watches);
if (loop->socket_set != NULL)
_dbus_socket_set_free (loop->socket_set);
dbus_free (loop);
return NULL;
}
loop->refcount = 1;
return loop;
}
DBusLoop *
_dbus_loop_ref (DBusLoop *loop)
{
_dbus_assert (loop != NULL);
_dbus_assert (loop->refcount > 0);
loop->refcount += 1;
return loop;
}
void
_dbus_loop_unref (DBusLoop *loop)
{
_dbus_assert (loop != NULL);
_dbus_assert (loop->refcount > 0);
loop->refcount -= 1;
if (loop->refcount == 0)
{
while (loop->need_dispatch)
{
DBusConnection *connection = _dbus_list_pop_first (&loop->need_dispatch);
dbus_connection_unref (connection);
}
_dbus_hash_table_unref (loop->watches);
_dbus_socket_set_free (loop->socket_set);
dbus_free (loop);
}
}
static DBusList **
ensure_watch_table_entry (DBusLoop *loop,
int fd)
{
DBusList **watches;
watches = _dbus_hash_table_lookup_int (loop->watches, fd);
if (watches == NULL)
{
watches = dbus_new0 (DBusList *, 1);
if (watches == NULL)
return watches;
if (!_dbus_hash_table_insert_int (loop->watches, fd, watches))
{
dbus_free (watches);
watches = NULL;
}
}
return watches;
}
static void
cull_watches_for_invalid_fd (DBusLoop *loop,
int fd)
{
DBusList *link;
DBusList **watches;
_dbus_warn ("invalid request, socket fd %d not open\n", fd);
watches = _dbus_hash_table_lookup_int (loop->watches, fd);
if (watches != NULL)
{
for (link = _dbus_list_get_first_link (watches);
link != NULL;
link = _dbus_list_get_next_link (watches, link))
_dbus_watch_invalidate (link->data);
}
_dbus_hash_table_remove_int (loop->watches, fd);
}
static dbus_bool_t
gc_watch_table_entry (DBusLoop *loop,
DBusList **watches,
int fd)
{
/* If watches is already NULL we have nothing to do */
if (watches == NULL)
return FALSE;
/* We can't GC hash table entries if they're non-empty lists */
if (*watches != NULL)
return FALSE;
_dbus_hash_table_remove_int (loop->watches, fd);
return TRUE;
}
static void
refresh_watches_for_fd (DBusLoop *loop,
DBusList **watches,
int fd)
{
DBusList *link;
unsigned int flags = 0;
dbus_bool_t interested = FALSE;
_dbus_assert (fd != -1);
if (watches == NULL)
watches = _dbus_hash_table_lookup_int (loop->watches, fd);
/* we allocated this in the first _dbus_loop_add_watch for the fd, and keep
* it until there are none left */
_dbus_assert (watches != NULL);
for (link = _dbus_list_get_first_link (watches);
link != NULL;
link = _dbus_list_get_next_link (watches, link))
{
if (dbus_watch_get_enabled (link->data) &&
!_dbus_watch_get_oom_last_time (link->data))
{
flags |= dbus_watch_get_flags (link->data);
interested = TRUE;
}
}
if (interested)
_dbus_socket_set_enable (loop->socket_set, fd, flags);
else
_dbus_socket_set_disable (loop->socket_set, fd);
}
dbus_bool_t
_dbus_loop_add_watch (DBusLoop *loop,
DBusWatch *watch)
{
int fd;
DBusList **watches;
fd = dbus_watch_get_socket (watch);
_dbus_assert (fd != -1);
watches = ensure_watch_table_entry (loop, fd);
if (watches == NULL)
return FALSE;
if (!_dbus_list_append (watches, _dbus_watch_ref (watch)))
{
_dbus_watch_unref (watch);
gc_watch_table_entry (loop, watches, fd);
return FALSE;
}
if (_dbus_list_length_is_one (watches))
{
if (!_dbus_socket_set_add (loop->socket_set, fd,
dbus_watch_get_flags (watch),
dbus_watch_get_enabled (watch)))
{
_dbus_hash_table_remove_int (loop->watches, fd);
return FALSE;
}
}
else
{
/* we're modifying, not adding, which can't fail with OOM */
refresh_watches_for_fd (loop, watches, fd);
}
loop->callback_list_serial += 1;
loop->watch_count += 1;
return TRUE;
}
void
_dbus_loop_toggle_watch (DBusLoop *loop,
DBusWatch *watch)
{
refresh_watches_for_fd (loop, NULL, dbus_watch_get_socket (watch));
}
void
_dbus_loop_remove_watch (DBusLoop *loop,
DBusWatch *watch)
{
DBusList **watches;
DBusList *link;
int fd;
/* This relies on people removing watches before they invalidate them,
* which has been safe since fd.o #33336 was fixed. Assert about it
* so we don't regress. */
fd = dbus_watch_get_socket (watch);
_dbus_assert (fd != -1);
watches = _dbus_hash_table_lookup_int (loop->watches, fd);
if (watches != NULL)
{
link = _dbus_list_get_first_link (watches);
while (link != NULL)
{
DBusList *next = _dbus_list_get_next_link (watches, link);
DBusWatch *this = link->data;
if (this == watch)
{
_dbus_list_remove_link (watches, link);
loop->callback_list_serial += 1;
loop->watch_count -= 1;
_dbus_watch_unref (this);
/* if that was the last watch for that fd, drop the hash table
* entry, and stop reserving space for it in the socket set */
if (gc_watch_table_entry (loop, watches, fd))
{
_dbus_socket_set_remove (loop->socket_set, fd);
}
return;
}
link = next;
}
}
_dbus_warn ("could not find watch %p to remove\n", watch);
}
dbus_bool_t
_dbus_loop_add_timeout (DBusLoop *loop,
DBusTimeout *timeout)
{
TimeoutCallback *tcb;
tcb = timeout_callback_new (timeout);
if (tcb == NULL)
return FALSE;
if (_dbus_list_append (&loop->timeouts, tcb))
{
loop->callback_list_serial += 1;
loop->timeout_count += 1;
}
else
{
timeout_callback_free (tcb);
return FALSE;
}
return TRUE;
}
void
_dbus_loop_remove_timeout (DBusLoop *loop,
DBusTimeout *timeout)
{
DBusList *link;
link = _dbus_list_get_first_link (&loop->timeouts);
while (link != NULL)
{
DBusList *next = _dbus_list_get_next_link (&loop->timeouts, link);
TimeoutCallback *this = link->data;
if (this->timeout == timeout)
{
_dbus_list_remove_link (&loop->timeouts, link);
loop->callback_list_serial += 1;
loop->timeout_count -= 1;
timeout_callback_free (this);
return;
}
link = next;
}
_dbus_warn ("could not find timeout %p to remove\n", timeout);
}
/* Convolutions from GLib, there really must be a better way
* to do this.
*/
static dbus_bool_t
check_timeout (unsigned long tv_sec,
unsigned long tv_usec,
TimeoutCallback *tcb,
int *timeout)
{
long sec_remaining;
long msec_remaining;
unsigned long expiration_tv_sec;
unsigned long expiration_tv_usec;
long interval_seconds;
long interval_milliseconds;
int interval;
/* I'm pretty sure this function could suck (a lot) less */
interval = dbus_timeout_get_interval (tcb->timeout);
interval_seconds = interval / 1000L;
interval_milliseconds = interval % 1000L;
expiration_tv_sec = tcb->last_tv_sec + interval_seconds;
expiration_tv_usec = tcb->last_tv_usec + interval_milliseconds * 1000;
if (expiration_tv_usec >= 1000000)
{
expiration_tv_usec -= 1000000;
expiration_tv_sec += 1;
}
sec_remaining = expiration_tv_sec - tv_sec;
/* need to force this to be signed, as it is intended to sometimes
* produce a negative result
*/
msec_remaining = ((long) expiration_tv_usec - (long) tv_usec) / 1000L;
#if MAINLOOP_SPEW
_dbus_verbose ("Interval is %ld seconds %ld msecs\n",
interval_seconds,
interval_milliseconds);
_dbus_verbose ("Now is %lu seconds %lu usecs\n",
tv_sec, tv_usec);
_dbus_verbose ("Last is %lu seconds %lu usecs\n",
tcb->last_tv_sec, tcb->last_tv_usec);
_dbus_verbose ("Exp is %lu seconds %lu usecs\n",
expiration_tv_sec, expiration_tv_usec);
_dbus_verbose ("Pre-correction, sec_remaining %ld msec_remaining %ld\n",
sec_remaining, msec_remaining);
#endif
/* We do the following in a rather convoluted fashion to deal with
* the fact that we don't have an integral type big enough to hold
* the difference of two timevals in milliseconds.
*/
if (sec_remaining < 0 || (sec_remaining == 0 && msec_remaining < 0))
{
*timeout = 0;
}
else
{
if (msec_remaining < 0)
{
msec_remaining += 1000;
sec_remaining -= 1;
}
if (sec_remaining > (_DBUS_INT_MAX / 1000) ||
msec_remaining > _DBUS_INT_MAX)
*timeout = _DBUS_INT_MAX;
else
*timeout = sec_remaining * 1000 + msec_remaining;
}
if (*timeout > interval)
{
/* This indicates that the system clock probably moved backward */
_dbus_verbose ("System clock set backward! Resetting timeout.\n");
tcb->last_tv_sec = tv_sec;
tcb->last_tv_usec = tv_usec;
*timeout = interval;
}
#if MAINLOOP_SPEW
_dbus_verbose (" timeout expires in %d milliseconds\n", *timeout);
#endif
return *timeout == 0;
}
dbus_bool_t
_dbus_loop_dispatch (DBusLoop *loop)
{
#if MAINLOOP_SPEW
_dbus_verbose (" %d connections to dispatch\n", _dbus_list_get_length (&loop->need_dispatch));
#endif
if (loop->need_dispatch == NULL)
return FALSE;
next:
while (loop->need_dispatch != NULL)
{
DBusConnection *connection = _dbus_list_pop_first (&loop->need_dispatch);
while (TRUE)
{
DBusDispatchStatus status;
status = dbus_connection_dispatch (connection);
if (status == DBUS_DISPATCH_COMPLETE)
{
dbus_connection_unref (connection);
goto next;
}
else
{
if (status == DBUS_DISPATCH_NEED_MEMORY)
_dbus_wait_for_memory ();
}
}
}
return TRUE;
}
dbus_bool_t
_dbus_loop_queue_dispatch (DBusLoop *loop,
DBusConnection *connection)
{
if (_dbus_list_append (&loop->need_dispatch, connection))
{
dbus_connection_ref (connection);
return TRUE;
}
else
return FALSE;
}
/* Returns TRUE if we invoked any timeouts or have ready file
* descriptors, which is just used in test code as a debug hack
*/
dbus_bool_t
_dbus_loop_iterate (DBusLoop *loop,
dbus_bool_t block)
{
#define N_STACK_DESCRIPTORS 64
dbus_bool_t retval;
DBusSocketEvent ready_fds[N_STACK_DESCRIPTORS];
int i;
DBusList *link;
int n_ready;
int initial_serial;
long timeout;
int orig_depth;
retval = FALSE;
orig_depth = loop->depth;
#if MAINLOOP_SPEW
_dbus_verbose ("Iteration block=%d depth=%d timeout_count=%d watch_count=%d\n",
block, loop->depth, loop->timeout_count, loop->watch_count);
#endif
if (_dbus_hash_table_get_n_entries (loop->watches) == 0 &&
loop->timeouts == NULL)
goto next_iteration;
timeout = -1;
if (loop->timeout_count > 0)
{
unsigned long tv_sec;
unsigned long tv_usec;
_dbus_get_monotonic_time (&tv_sec, &tv_usec);
link = _dbus_list_get_first_link (&loop->timeouts);
while (link != NULL)
{
DBusList *next = _dbus_list_get_next_link (&loop->timeouts, link);
TimeoutCallback *tcb = link->data;
if (dbus_timeout_get_enabled (tcb->timeout))
{
int msecs_remaining;
check_timeout (tv_sec, tv_usec, tcb, &msecs_remaining);
if (timeout < 0)
timeout = msecs_remaining;
else
timeout = MIN (msecs_remaining, timeout);
#if MAINLOOP_SPEW
_dbus_verbose (" timeout added, %d remaining, aggregate timeout %ld\n",
msecs_remaining, timeout);
#endif
_dbus_assert (timeout >= 0);
if (timeout == 0)
break; /* it's not going to get shorter... */
}
#if MAINLOOP_SPEW
else
{
_dbus_verbose (" skipping disabled timeout\n");
}
#endif
link = next;
}
}
/* Never block if we have stuff to dispatch */
if (!block || loop->need_dispatch != NULL)
{
timeout = 0;
#if MAINLOOP_SPEW
_dbus_verbose (" timeout is 0 as we aren't blocking\n");
#endif
}
/* if a watch was OOM last time, don't wait longer than the OOM
* wait to re-enable it
*/
if (loop->oom_watch_pending)
timeout = MIN (timeout, _dbus_get_oom_wait ());
#if MAINLOOP_SPEW
_dbus_verbose (" polling on %d descriptors timeout %ld\n", n_fds, timeout);
#endif
n_ready = _dbus_socket_set_poll (loop->socket_set, ready_fds,
_DBUS_N_ELEMENTS (ready_fds), timeout);
/* re-enable any watches we skipped this time */
if (loop->oom_watch_pending)
{
DBusHashIter hash_iter;
loop->oom_watch_pending = FALSE;
_dbus_hash_iter_init (loop->watches, &hash_iter);
while (_dbus_hash_iter_next (&hash_iter))
{
DBusList **watches;
int fd;
dbus_bool_t changed;
changed = FALSE;
fd = _dbus_hash_iter_get_int_key (&hash_iter);
watches = _dbus_hash_iter_get_value (&hash_iter);
for (link = _dbus_list_get_first_link (watches);
link != NULL;
link = _dbus_list_get_next_link (watches, link))
{
DBusWatch *watch = link->data;
if (_dbus_watch_get_oom_last_time (watch))
{
_dbus_watch_set_oom_last_time (watch, FALSE);
changed = TRUE;
}
}
if (changed)
refresh_watches_for_fd (loop, watches, fd);
}
retval = TRUE; /* return TRUE here to keep the loop going,
* since we don't know the watch was inactive */
}
initial_serial = loop->callback_list_serial;
if (loop->timeout_count > 0)
{
unsigned long tv_sec;
unsigned long tv_usec;
_dbus_get_monotonic_time (&tv_sec, &tv_usec);
/* It'd be nice to avoid this O(n) thingy here */
link = _dbus_list_get_first_link (&loop->timeouts);
while (link != NULL)
{
DBusList *next = _dbus_list_get_next_link (&loop->timeouts, link);
TimeoutCallback *tcb = link->data;
if (initial_serial != loop->callback_list_serial)
goto next_iteration;
if (loop->depth != orig_depth)
goto next_iteration;
if (dbus_timeout_get_enabled (tcb->timeout))
{
int msecs_remaining;
if (check_timeout (tv_sec, tv_usec,
tcb, &msecs_remaining))
{
/* Save last callback time and fire this timeout */
tcb->last_tv_sec = tv_sec;
tcb->last_tv_usec = tv_usec;
#if MAINLOOP_SPEW
_dbus_verbose (" invoking timeout\n");
#endif
/* can theoretically return FALSE on OOM, but we just
* let it fire again later - in practice that's what
* every wrapper callback in dbus-daemon used to do */
dbus_timeout_handle (tcb->timeout);
retval = TRUE;
}
else
{
#if MAINLOOP_SPEW
_dbus_verbose (" timeout has not expired\n");
#endif
}
}
#if MAINLOOP_SPEW
else
{
_dbus_verbose (" skipping invocation of disabled timeout\n");
}
#endif
link = next;
}
}
if (n_ready > 0)
{
for (i = 0; i < n_ready; i++)
{
DBusList **watches;
DBusList *next;
unsigned int condition;
dbus_bool_t any_oom;
/* FIXME I think this "restart if we change the watches"
* approach could result in starving watches
* toward the end of the list.
*/
if (initial_serial != loop->callback_list_serial)
goto next_iteration;
if (loop->depth != orig_depth)
goto next_iteration;
_dbus_assert (ready_fds[i].flags != 0);
if (_DBUS_UNLIKELY (ready_fds[i].flags & _DBUS_WATCH_NVAL))
{
cull_watches_for_invalid_fd (loop, ready_fds[i].fd);
goto next_iteration;
}
condition = ready_fds[i].flags;
_dbus_assert ((condition & _DBUS_WATCH_NVAL) == 0);
/* condition may still be 0 if we got some
* weird POLLFOO thing like POLLWRBAND
*/
if (condition == 0)
continue;
watches = _dbus_hash_table_lookup_int (loop->watches,
ready_fds[i].fd);
if (watches == NULL)
continue;
any_oom = FALSE;
for (link = _dbus_list_get_first_link (watches);
link != NULL;
link = next)
{
DBusWatch *watch = link->data;
next = _dbus_list_get_next_link (watches, link);
if (dbus_watch_get_enabled (watch))
{
dbus_bool_t oom;
oom = !dbus_watch_handle (watch, condition);
if (oom)
{
_dbus_watch_set_oom_last_time (watch, TRUE);
loop->oom_watch_pending = TRUE;
any_oom = TRUE;
}
#if MAINLOOP_SPEW
_dbus_verbose (" Invoked watch, oom = %d\n", oom);
#endif
retval = TRUE;
/* We re-check this every time, in case the callback
* added/removed watches, which might make our position in
* the linked list invalid. See the FIXME above. */
if (initial_serial != loop->callback_list_serial ||
loop->depth != orig_depth)
{
if (any_oom)
refresh_watches_for_fd (loop, NULL, ready_fds[i].fd);
goto next_iteration;
}
}
}
if (any_oom)
refresh_watches_for_fd (loop, watches, ready_fds[i].fd);
}
}
next_iteration:
#if MAINLOOP_SPEW
_dbus_verbose (" moving to next iteration\n");
#endif
if (_dbus_loop_dispatch (loop))
retval = TRUE;
#if MAINLOOP_SPEW
_dbus_verbose ("Returning %d\n", retval);
#endif
return retval;
}
void
_dbus_loop_run (DBusLoop *loop)
{
int our_exit_depth;
_dbus_assert (loop->depth >= 0);
_dbus_loop_ref (loop);
our_exit_depth = loop->depth;
loop->depth += 1;
_dbus_verbose ("Running main loop, depth %d -> %d\n",
loop->depth - 1, loop->depth);
while (loop->depth != our_exit_depth)
_dbus_loop_iterate (loop, TRUE);
_dbus_loop_unref (loop);
}
void
_dbus_loop_quit (DBusLoop *loop)
{
_dbus_assert (loop->depth > 0);
loop->depth -= 1;
_dbus_verbose ("Quit main loop, depth %d -> %d\n",
loop->depth + 1, loop->depth);
}
int
_dbus_get_oom_wait (void)
{
#ifdef DBUS_BUILD_TESTS
/* make tests go fast */
return 0;
#else
return 500;
#endif
}
void
_dbus_wait_for_memory (void)
{
_dbus_verbose ("Waiting for more memory\n");
_dbus_sleep_milliseconds (_dbus_get_oom_wait ());
}
#endif /* !DOXYGEN_SHOULD_SKIP_THIS */