 bindings/python/fiu.py         |    7 +
 libfiu/Makefile                |   21 ++-
 libfiu/fiu.c                   |  353 +++++++++++---------------------
 libfiu/hash.c                  |  447 ++++++++++++++++++++++++++++++++++++++++
 libfiu/hash.h                  |   34 +++
 libfiu/wtable.c                |  358 ++++++++++++++++++++++++++++++++
 libfiu/wtable.h                |   24 +++
 tests/Makefile                 |    2 +-
 tests/perf-fsck.py             |  148 +++++++++++++
 tests/test-manyfps.py          |   25 +++
 tests/test-onetime.py          |   17 ++
 tests/test-parallel-wildcard.c |  211 +++++++++++++++++++
 tests/test-parallel.c          |  176 ++++++++++++++++
 tests/test-wildcards.py        |   44 ++++
 utils/fiu-ctrl                 |   22 ++-
 15 files changed, 1638 insertions(+), 251 deletions(-)

diff --git a/bindings/python/fiu.py b/bindings/python/fiu.py
index fda5639..0b0a308 100644
--- a/bindings/python/fiu.py
+++ b/bindings/python/fiu.py
@@ -25,6 +25,13 @@ def failinfo(name):
 	Python."""
 	return _ll.failinfo()
 
+class Flags:
+	"""Contains the valid flag constants.
+
+	ONETIME: This point of failure is disabled immediately after failing once.
+	"""
+	ONETIME = _ll.FIU_ONETIME
+
 
 # To be sure failinfo doesn't dissapear from under our feet, we keep a
 # name -> failinfo table. See fiu_ll's comments for more details.
diff --git a/libfiu/Makefile b/libfiu/Makefile
index 1288b8d..086a0b7 100644
--- a/libfiu/Makefile
+++ b/libfiu/Makefile
@@ -1,13 +1,18 @@
 
-CFLAGS += -std=c99 -pedantic -Wall -O3
+CFLAGS += -std=c99 -pedantic -Wall
 ALL_CFLAGS = -D_XOPEN_SOURCE=600 -fPIC -DFIU_ENABLE=1 $(CFLAGS)
 
-ifdef DEBUG
-ALL_CFLAGS += -g
-endif
+# Optimization CFLAGS, only used when doing a non-debug build.
+OPT_CFLAGS = -O3
 
-ifdef PROFILE
-ALL_CFLAGS += -g -pg -fprofile-arcs -ftest-coverage
+ifdef DEBUG
+    ALL_CFLAGS += -g
+else
+   ifdef PROFILE
+        ALL_CFLAGS += -g -pg -fprofile-arcs -ftest-coverage
+   else
+       ALL_CFLAGS += $(OPT_CFLAGS)
+   endif
 endif
 
 
@@ -21,7 +26,7 @@ DESTDIR=$(PREFIX)
 INSTALL=install
 
 
-OBJS = fiu.o fiu-rc.o backtrace.o
+OBJS = fiu.o fiu-rc.o backtrace.o wtable.o hash.o
 
 
 ifneq ($(V), 1)
@@ -30,7 +35,7 @@ else
 	NICE_CC = $(CC)
 endif
 
-LIB_VER=0.91
+LIB_VER=0.92
 LIB_SO_VER=0
 
 
diff --git a/libfiu/fiu.c b/libfiu/fiu.c
index 68cab04..ce89112 100644
--- a/libfiu/fiu.c
+++ b/libfiu/fiu.c
@@ -12,6 +12,7 @@
 #include "fiu.h"
 #include "fiu-control.h"
 #include "internal.h"
+#include "wtable.h"
 
 
 /* Different methods to decide when a point of failure fails */
@@ -30,6 +31,10 @@ struct pf_info {
 	void *failinfo;
 	unsigned int flags;
 
+	/* Lock and failed flag, used only when flags & FIU_ONETIME. */
+	pthread_mutex_t lock;
+	bool failed_once;
+
 	/* How to decide when this point of failure fails, and the information
 	 * needed to take the decision */
 	enum pf_method method;
@@ -49,17 +54,52 @@ struct pf_info {
 	} minfo;
 };
 
+/* Creates a new pf_info.
+ * Only the common fields are filled, the caller should take care of the
+ * method-specific ones. For internal use only. */
+static struct pf_info *pf_create(const char *name, int failnum,
+		void *failinfo, unsigned int flags, enum pf_method method)
+{
+	struct pf_info *pf;
+
+	rec_count++;
+
+	pf = malloc(sizeof(struct pf_info));
+	if (pf == NULL)
+		goto exit;
+
+	pf->name = strdup(name);
+	if (pf->name == NULL) {
+		free(pf);
+		pf = NULL;
+		goto exit;
+	}
+
+	pf->namelen = strlen(name);
+	pf->failnum = failnum;
+	pf->failinfo = failinfo;
+	pf->flags = flags;
+	pf->method = method;
+
+	/* These two are only use when flags & FIU_ONETIME. */
+	pthread_mutex_init(&pf->lock, NULL);
+	pf->failed_once = false;
+
+exit:
+	rec_count--;
+	return pf;
+}
+
+static void pf_free(struct pf_info *pf) {
+	free(pf->name);
+	pthread_mutex_destroy(&pf->lock);
+	free(pf);
+}
+
 
-/* Array used to keep the information about the enabled points of failure.
- * It's an array because we assume it's going to be short enough for the
- * linear lookup not matter.
- * In the future, if it turns out it's normal that it grows large enough, we
- * may be interested in a more sophisticated structure like a hash table
- * and/or a bloom filter. */
-static struct pf_info *enabled_fails = NULL;
-static struct pf_info *enabled_fails_last = NULL;
-static size_t enabled_fails_len = 0;
-static size_t enabled_fails_nfree = 0;
+/* Table used to keep the information about the enabled points of failure.
+ * It is protected by enabled_fails_lock. */
+wtable_t *enabled_fails = NULL;
 static pthread_rwlock_t enabled_fails_lock = PTHREAD_RWLOCK_INITIALIZER;
 
 #define ef_rlock() do { pthread_rwlock_rdlock(&enabled_fails_lock); } while (0)
@@ -67,6 +107,7 @@ static pthread_rwlock_t enabled_fails_lock = PTHREAD_RWLOCK_INITIALIZER;
 #define ef_runlock() do { pthread_rwlock_unlock(&enabled_fails_lock); } while (0)
 #define ef_wunlock() do { pthread_rwlock_unlock(&enabled_fails_lock); } while (0)
 
+
 /* To prevent unwanted recursive calls that would deadlock, we use a
  * thread-local recursion count. Unwanted recursive calls can result from
  * using functions that have been modified to call fiu_fail(), which can
@@ -83,15 +124,6 @@ static pthread_rwlock_t enabled_fails_lock = PTHREAD_RWLOCK_INITIALIZER;
 __thread int rec_count = 0;
 
 
-/* Maximum number of free elements in enabled_fails (used to decide when to
- * shrink). */
-#define EF_MAX_FREE 3
-
-/* How much to grow enabled_fails by each time, it's recommended that this is
- * less than EF_MAX_FREE. */
-#define EF_GROW 2
-
-
 /* Used to keep the last failinfo via TLS */
 static pthread_key_t last_failinfo_key;
 
@@ -100,80 +132,6 @@ static pthread_key_t last_failinfo_key;
  * Miscelaneous internal functions
  */
 
-/* Disables the given pf_info, assuming it's inside enabled_fails. Must be
- * called with enabled_fails_lock acquired. */
-static void disable_pf(struct pf_info *pf)
-{
-	/* free the name we've allocated in setup_fail() via strdup() */
-	free(pf->name);
-	pf->name = NULL;
-	pf->namelen = 0;
-	pf->failnum = 0;
-	pf->failinfo = NULL;
-	pf->flags = 0;
-}
-
-/* Return the last position where s1 and s2 match. */
-static unsigned int strlast(const char *s1, const char *s2)
-{
-	unsigned int i = 0;
-
-	while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) {
-		i++;
-		s1++;
-		s2++;
-	}
-
-	return i;
-}
-
-/* Checks if pf's name matches the one given. pf->name can be NULL. */
-static int name_matches(const struct pf_info *pf, const char *name, int exact)
-{
-	if (pf->name == NULL || name == NULL)
-		return 0;
-
-	if (exact || pf->name[pf->namelen - 1] != '*')
-		return strcmp(pf->name, name) == 0;
-
-	/* Inexact match */
-	return strlast(pf->name, name) >= pf->namelen - 1;
-}
-
-/* Shrink enabled_fails, used when it has too many free elements. Must be
- * called with enabled_fails_lock acquired. */
-static int shrink_enabled_fails(void)
-{
-	int i;
-	size_t new_len;
-	struct pf_info *new, *pf;
-
-	new_len = enabled_fails_len - enabled_fails_nfree + EF_GROW;
-
-	new = malloc(new_len * sizeof(struct pf_info));
-	if (new == NULL)
-		return -1;
-
-	i = 0;
-	for (pf = enabled_fails; pf <= enabled_fails_last; pf++) {
-		if (pf->name == NULL)
-			continue;
-
-		memcpy(new + i, pf, sizeof(struct pf_info));
-		i++;
-	}
-
-	memset(new + i, 0, (new_len - i) * sizeof(struct pf_info));
-
-	free(enabled_fails);
-	enabled_fails = new;
-	enabled_fails_len = new_len;
-	enabled_fails_last = new + new_len - 1;
-	enabled_fails_nfree = EF_GROW;
-
-	return 0;
-}
-
 /* Determines if the given address is within the function code. */
 static int pc_in_func(struct pf_info *pf, void *pc)
 {
@@ -271,10 +229,7 @@ int fiu_init(unsigned int flags)
 
 	pthread_key_create(&last_failinfo_key, NULL);
 
-	enabled_fails = NULL;
-	enabled_fails_last = NULL;
-	enabled_fails_len = 0;
-	enabled_fails_nfree = 0;
+	enabled_fails = wtable_create((void (*)(void *)) pf_free);
 
 	if (pthread_atfork(NULL, NULL, atfork_child) != 0) {
 		ef_wunlock();
@@ -310,41 +265,59 @@ int fiu_fail(const char *name)
 
 	ef_rlock();
 
+	/* It can happen that someone calls fiu_fail() before fiu_init(); we
+	 * don't want to crash so we just exit. */
 	if (enabled_fails == NULL) {
+		goto exit;
+	}
+
+	pf = wtable_get(enabled_fails, name);
+
+	/* None found. */
+	if (pf == NULL) {
 		ef_runlock();
 		rec_count--;
 		return 0;
 	}
 
-	for (pf = enabled_fails; pf <= enabled_fails_last; pf++) {
-		if (name_matches(pf, name, 0)) {
-			switch (pf->method) {
-			case PF_ALWAYS:
-				goto exit_fail;
-				break;
-			case PF_PROB:
-				if (pf->minfo.probability > randd())
-					goto exit_fail;
-				break;
-			case PF_EXTERNAL:
-				if (pf->minfo.external_cb(pf->name,
-						&(pf->failnum),
-						&(pf->failinfo),
-						&(pf->flags)))
-					goto exit_fail;
-				break;
-			case PF_STACK:
-				if (should_stack_fail(pf))
-					goto exit_fail;
-				break;
-			default:
-				break;
-			}
+	if (pf->flags & FIU_ONETIME) {
+		pthread_mutex_lock(&pf->lock);
+		if (pf->failed_once) {
+			pthread_mutex_unlock(&pf->lock);
+			goto exit;
+		}
+		/* We leave it locked so we don't accidentally fail this
+		 * point twice. */
+	}
 
+	switch (pf->method) {
+		case PF_ALWAYS:
+			goto exit_fail;
+			break;
+		case PF_PROB:
+			if (pf->minfo.probability > randd())
+				goto exit_fail;
+			break;
+		case PF_EXTERNAL:
+			if (pf->minfo.external_cb(pf->name,
+					&(pf->failnum),
+					&(pf->failinfo),
+					&(pf->flags)))
+				goto exit_fail;
+			break;
+		case PF_STACK:
+			if (should_stack_fail(pf))
+				goto exit_fail;
+			break;
+		default:
 			break;
-		}
 	}
 
+	if (pf->flags & FIU_ONETIME) {
+		pthread_mutex_unlock(&pf->lock);
+	}
+
+exit:
 	ef_runlock();
 	rec_count--;
 	return 0;
@@ -355,8 +328,8 @@ exit_fail:
 	failnum = pf->failnum;
 
 	if (pf->flags & FIU_ONETIME) {
-		disable_pf(pf);
-		enabled_fails_nfree++;
+		pf->failed_once = true;
+		pthread_mutex_unlock(&pf->lock);
 	}
 
 	ef_runlock();
@@ -375,94 +348,20 @@ void *fiu_failinfo(void)
  * Control API
  */
 
-/* Sets up the given pf.
- * Only the common fields are filled, the caller should take care of the
- * method-specific ones. For internal use only. */
-static int setup_fail(struct pf_info *pf, const char *name, int failnum,
-		void *failinfo, unsigned int flags, enum pf_method method)
-{
-	pf->name = strdup(name);
-	if (pf->name == NULL)
-		return -1;
-
-	pf->namelen = strlen(name);
-	pf->failnum = failnum;
-	pf->failinfo = failinfo;
-	pf->flags = flags;
-	pf->method = method;
-
-	return 0;
-}
-
-/* Creates a new pf in the enabled_fails table.
- * Only the common fields are filled, the caller should take care of the
- * method-specific ones. For internal use only. */
-static struct pf_info *insert_new_fail(const char *name, int failnum,
-		void *failinfo, unsigned int flags, enum pf_method method)
+/* Inserts a pf into in the enabled_fails table.
+ * Returns 0 on success, -1 on failure. */
+static int insert_pf(struct pf_info *pf)
 {
-	struct pf_info *pf = NULL;
-	int rv = -1;
-	size_t prev_len;
+	bool success;
 
 	rec_count++;
 
-	/* See if it's already there and update the data if so, or if we have
-	 * a free spot where to put it */
 	ef_wlock();
-	if (enabled_fails != NULL && enabled_fails_nfree > 0) {
-		for (pf = enabled_fails; pf <= enabled_fails_last; pf++) {
-			if (pf->name == NULL || strcmp(pf->name, name) == 0) {
-				rv = setup_fail(pf, name, failnum, failinfo,
-						flags, method);
-				if (rv != 0) {
-					pf = NULL;
-					goto exit;
-				}
-
-				enabled_fails_nfree--;
-				goto exit;
-			}
-		}
-
-		/* There should be a free slot, but couldn't find one! This
-		 * shouldn't happen */
-		pf = NULL;
-		goto exit;
-	}
-
-	/* There are no free slots in enabled_fails, so we must grow it */
-	enabled_fails = realloc(enabled_fails,
-			(enabled_fails_len + EF_GROW) * sizeof(struct pf_info));
-	if (enabled_fails == NULL) {
-		enabled_fails_last = NULL;
-		enabled_fails_len = 0;
-		enabled_fails_nfree = 0;
-		pf = NULL;
-		goto exit;
-	}
-
-	prev_len = enabled_fails_len;
-	enabled_fails_len += EF_GROW;
-	enabled_fails_nfree = EF_GROW;
-
-	memset(enabled_fails + prev_len, 0,
-			EF_GROW * sizeof(struct pf_info));
-
-	enabled_fails_last = enabled_fails + enabled_fails_len - 1;
-
-	pf = enabled_fails + prev_len;
-	rv = setup_fail(pf, name, failnum, failinfo, flags, method);
-	if (rv != 0) {
-		pf = NULL;
-		goto exit;
-	}
-
-	enabled_fails_nfree--;
-
-exit:
+	success = wtable_set(enabled_fails, pf->name, pf);
 	ef_wunlock();
+
 	rec_count--;
-	return pf;
+	return success ? 0 : -1;
 }
 
 /* Makes the given name fail. */
@@ -471,11 +370,11 @@ int fiu_enable(const char *name, int failnum, void *failinfo,
 {
 	struct pf_info *pf;
 
-	pf = insert_new_fail(name, failnum, failinfo, flags, PF_ALWAYS);
+	pf = pf_create(name, failnum, failinfo, flags, PF_ALWAYS);
 	if (pf == NULL)
 		return -1;
 
-	return 0;
+	return insert_pf(pf);
 }
 
 /* Makes the given name fail with the given probability. */
@@ -484,12 +383,12 @@ int fiu_enable_random(const char *name, int failnum, void *failinfo,
 {
 	struct pf_info *pf;
 
-	pf = insert_new_fail(name, failnum, failinfo, flags, PF_PROB);
+	pf = pf_create(name, failnum, failinfo, flags, PF_PROB);
 	if (pf == NULL)
 		return -1;
 
 	pf->minfo.probability = probability;
-	return 0;
+	return insert_pf(pf);
 }
 
 /* Makes the given name fail when the external function returns != 0. */
@@ -498,12 +397,12 @@ int fiu_enable_external(const char *name, int failnum, void *failinfo,
 {
 	struct pf_info *pf;
 
-	pf = insert_new_fail(name, failnum, failinfo, flags, PF_EXTERNAL);
+	pf = pf_create(name, failnum, failinfo, flags, PF_EXTERNAL);
 	if (pf == NULL)
 		return -1;
 
 	pf->minfo.external_cb = external_cb;
-	return 0;
+	return insert_pf(pf);
 }
 
 /* Makes the given name fail when func is in the stack at func_pos.
@@ -520,7 +419,7 @@ int fiu_enable_stack(const char *name, int failnum, void *failinfo,
 	if (backtrace_works((void (*)()) fiu_enable_stack) == 0)
 		return -1;
 
-	pf = insert_new_fail(name, failnum, failinfo, flags, PF_STACK);
+	pf = pf_create(name, failnum, failinfo, flags, PF_STACK);
 	if (pf == NULL)
 		return -1;
 
@@ -530,7 +429,7 @@ int fiu_enable_stack(const char *name, int failnum, void *failinfo,
 	 * to make it work, see pc_in_func() above. */
 	pf->minfo.stack.func_end = get_func_end(func);
 	pf->minfo.stack.func_pos_in_stack = func_pos_in_stack;
-	return 0;
+	return insert_pf(pf);
 }
 
 /* Same as fiu_enable_stack(), but takes a function name. */
@@ -557,35 +456,17 @@ int fiu_enable_stack_by_name(const char *name, int failnum, void *failinfo,
 /* Makes the given name NOT fail. */
 int fiu_disable(const char *name)
 {
-	struct pf_info *pf;
+	bool success;
 
 	rec_count++;
 
-	/* just find the point of failure and mark it as free by setting its
-	 * name to NULL */
+	/* Just find the point of failure and remove it. */
 	ef_wlock();
-
-	if (enabled_fails == NULL) {
-		ef_wunlock();
-		rec_count--;
-		return -1;
-	}
-
-	for (pf = enabled_fails; pf <= enabled_fails_last; pf++) {
-		if (name_matches(pf, name, 1)) {
-			disable_pf(pf);
-			enabled_fails_nfree++;
-			if (enabled_fails_nfree > EF_MAX_FREE)
-				shrink_enabled_fails();
-			ef_wunlock();
-			rec_count--;
-			return 0;
-		}
-	}
-
+	success = wtable_del(enabled_fails, name);
 	ef_wunlock();
+
 	rec_count--;
-	return -1;
+	return success ? 0 : -1;
 }
 
 
diff --git a/libfiu/hash.c b/libfiu/hash.c
new file mode 100644
index 0000000..e8b037d
--- /dev/null
+++ b/libfiu/hash.c
@@ -0,0 +1,447 @@
+
+/*
+ * Generic, simple hash table.
+ *
+ * Takes \0-terminated strings as keys, and void * as values.
+ * It is tuned for a small number of elements (< 1000).
+ *
+ * It is NOT thread-safe.
+ */
+
+#include <sys/types.h>		/* for size_t */
+#include <stdint.h>		/* for [u]int*_t */
+#include <stdbool.h>		/* for bool */
+#include <stdlib.h>		/* for malloc() */
+#include <string.h>		/* for memcpy()/memcmp() */
+#include <stdio.h>		/* snprintf() */
+#include <pthread.h>		/* read-write locks */
+#include "hash.h"
+
+/* MurmurHash2, by Austin Appleby. The one we use.
+ * It has been modify to fit into the coding style, to work on uint32_t
+ * instead of ints, and the seed was fixed to a random number because it's not
+ * an issue for us. The author placed it in the public domain, so it's ok to
+ * use it here.
+ * http://sites.google.com/site/murmurhash/ */
+static uint32_t murmurhash2(const char *key, size_t len)
+{
+	const uint32_t m = 0x5bd1e995;
+	const int r = 24;
+	const uint32_t seed = 0x34a4b627;
+
+	// Initialize the hash to a 'random' value
+	uint32_t h = seed ^ len;
+
+	// Mix 4 bytes at a time into the hash
+	while (len >= 4) {
+		uint32_t k = *(uint32_t *) key;
+
+		k *= m;
+		k ^= k >> r;
+		k *= m;
+
+		h *= m;
+		h ^= k;
+
+		key += 4;
+		len -= 4;
+	}
+
+	// Handle the last few bytes of the input array
+	switch (len) {
+		case 3: h ^= key[2] << 16;
+		case 2: h ^= key[1] << 8;
+		case 1: h ^= key[0];
+			h *= m;
+	}
+
+	// Do a few final mixes of the hash to ensure the last few
+	// bytes are well-incorporated.
+	h ^= h >> 13;
+	h *= m;
+	h ^= h >> 15;
+
+	return h;
+}
+
+enum used_as {
+	NEVER = 0,
+	IN_USE = 1,
+	REMOVED = 2,
+};
+
+struct entry {
+	char *key;
+	void *value;
+	enum used_as in_use;
+};
+
+struct hash {
+	struct entry *entries;
+	size_t table_size;
+	size_t nentries;
+	void (*destructor)(void *);
+};
+
+
+/* Minimum table size. */
+#define MIN_SIZE 10
+
+/* Dumb destructor, used to simplify the code when no destructor is given. */
+static void dumb_destructor(void *value)
+{
+	return;
+}
+
+struct hash *hash_create(void (*destructor)(void *))
+{
+	struct hash *h = malloc(sizeof(struct hash));
+	if (h == NULL)
+		return NULL;
+
+	h->entries = malloc(sizeof(struct entry) * MIN_SIZE);
+	if (h->entries == NULL) {
+		free(h);
+		return NULL;
+	}
+
+	memset(h->entries, 0, sizeof(struct entry) * MIN_SIZE);
+
+	h->table_size = MIN_SIZE;
+	h->nentries = 0;
+
+	if (destructor == NULL)
+		destructor = dumb_destructor;
+
+	h->destructor = destructor;
+
+	return h;
+}
+
+void hash_free(struct hash *h)
+{
+	size_t i;
+	struct entry *entry;
+
+	for (i = 0; i < h->table_size; i++) {
+		entry = h->entries + i;
+		if (entry->in_use == IN_USE) {
+			h->destructor(entry->value);
+			free(entry->key);
+		}
+	}
+
+	free(h->entries);
+	free(h);
+}
+
+void *hash_get(struct hash *h, const char *key)
+{
+	size_t pos;
+	struct entry *entry;
+
+	pos = murmurhash2(key, strlen(key)) % h->table_size;
+
+	for (;;) {
+		entry = h->entries + pos;
+		if (entry->in_use == NEVER) {
+			/* We got to a entry never used, no match. */
+			return NULL;
+		} else if (entry->in_use == IN_USE &&
+				strcmp(key, entry->key) == 0) {
+			/* The key matches. */
+			return entry->value;
+		} else {
+			/* We use linear probing for now */
+			pos = (pos + 1) % h->table_size;
+		}
+	}
+
+	return NULL;
+}
+
+/* Internal version of hash_set.
+ * It uses the key as-is (it won't copy it), and it won't resize the array
+ * either. */
+static bool _hash_set(struct hash *h, char *key, void *value)
+{
+	size_t pos;
+	struct entry *entry;
+
+	pos = murmurhash2(key, strlen(key)) % h->table_size;
+
+	for (;;) {
+		entry = h->entries + pos;
+		if (entry->in_use != IN_USE) {
+			entry->in_use = IN_USE;
+			entry->key = key;
+			entry->value = value;
+			h->nentries++;
+			return true;
+		} else if (strcmp(key, entry->key) == 0) {
+			/* The key matches, override the value. */
+			h->destructor(entry->value);
+			entry->value = value;
+			return true;
+		} else {
+			/* The key doesn't match, linear probing. */
+			pos = (pos + 1) % h->table_size;
+		}
+	}
+
+	return false;
+}
+
+static bool resize_table(struct hash *h, size_t new_size)
+{
+	size_t i;
+	struct entry *old_entries, *e;
+	size_t old_size;
+
+	if (new_size < MIN_SIZE) {
+		/* Do not resize below minimum size */
+		return true;
+	}
+
+	old_entries = h->entries;
+	old_size = h->table_size;
+
+	h->entries = malloc(sizeof(struct entry) * new_size);
+	if (h->entries == NULL)
+		return false;
+
+	memset(h->entries, 0, sizeof(struct entry) * new_size);
+	h->table_size = new_size;
+	h->nentries = 0;
+
+	/* Insert the old entries into the new table. We use the internal
+	 * version _hash_set() to avoid copying the keys again. */
+	for (i = 0; i < old_size; i++) {
+		e = old_entries + i;
+		if (e->in_use == IN_USE)
+			_hash_set(h, e->key, e->value);
+	}
+
+	free(old_entries);
+
+	return true;
+}
+
+bool hash_set(struct hash *h, const char *key, void *value)
+{
+	if ((float) h->nentries / h->table_size > 0.7) {
+		/* If we're over 70% full, grow the table by 30% */
+		if (!resize_table(h, h->table_size * 1.3))
+			return false;
+	}
+
+	return _hash_set(h, strdup(key), value);
+}
+
+
+bool hash_del(struct hash *h, const char *key)
+{
+	size_t pos;
+	struct entry *entry;
+
+	pos = murmurhash2(key, strlen(key)) % h->table_size;
+
+	for (;;) {
+		entry = h->entries + pos;
+		if (entry->in_use == NEVER) {
+			/* We got to a never used key, not found. */
+			return false;
+		} else if (entry->in_use == IN_USE &&
+				strcmp(key, entry->key) == 0) {
+			/* The key matches, remove it. */
+			free(entry->key);
+			h->destructor(entry->value);
+			entry->key = NULL;
+			entry->value = NULL;
+			entry->in_use = REMOVED;
+			break;
+		} else {
+			/* The key doesn't match, linear probing. */
+			pos = (pos + 1) % h->table_size;
+		}
+	}
+
+	if (h->table_size > MIN_SIZE &&
+			(float) h->nentries / h->table_size < 0.5) {
+		/* If we're over 50% free, shrink. */
+		if (!resize_table(h, h->table_size * 0.8))
+			return false;
+	}
+
+	return true;
+
+}
+
+/* Generic, simple cache.
+ *
+ * It is implemented using a hash table and manipulating it directly when
+ * needed.
+ *
+ * It favours reads over writes, and it's very basic.
+ * It IS thread-safe.
+ */
+
+struct cache {
+	struct hash *hash;
+	size_t size;
+	pthread_rwlock_t lock;
+};
+
+struct cache *cache_create()
+{
+	struct cache *c;
+
+	c = malloc(sizeof(struct cache));
+	if (c == NULL)
+		return NULL;
+
+	c->hash = hash_create(NULL);
+	if (c->hash == NULL) {
+		free(c);
+		return NULL;
+	}
+
+	pthread_rwlock_init(&c->lock, NULL);
+
+	return c;
+}
+
+void cache_free(struct cache *c)
+{
+	hash_free(c->hash);
+	pthread_rwlock_destroy(&c->lock);
+	free(c);
+}
+
+/* Internal non-locking version of cache_invalidate(). */
+static void _cache_invalidate(struct cache *c)
+{
+	size_t i;
+	struct entry *entry;
+
+	for (i = 0; i < c->hash->table_size; i++) {
+		entry = c->hash->entries + i;
+		if (entry->in_use == IN_USE) {
+			free(entry->key);
+			entry->key = NULL;
+			entry->value = NULL;
+			entry->in_use = NEVER;
+		}
+	}
+}
+
+bool cache_invalidate(struct cache *c)
+{
+	pthread_rwlock_wrlock(&c->lock);
+	_cache_invalidate(c);
+	pthread_rwlock_unlock(&c->lock);
+
+	return true;
+}
+
+bool cache_resize(struct cache *c, size_t new_size)
+{
+	pthread_rwlock_wrlock(&c->lock);
+	if (new_size > c->size) {
+		if (resize_table(c->hash, new_size)) {
+			c->size = new_size;
+			goto success;
+		}
+	} else {
+		/* TODO: Implement shrinking. We just invalidate everything
+		 * for now, and then resize. */
+		_cache_invalidate(c);
+
+		if (resize_table(c->hash, new_size)) {
+			c->size = new_size;
+			goto success;
+		}
+	}
+
+	pthread_rwlock_unlock(&c->lock);
+	return false;
+
+success:
+	pthread_rwlock_unlock(&c->lock);
+	return true;
+}
+
+struct entry *entry_for_key(struct cache *c, const char *key)
+{
+	size_t pos;
+	struct entry *entry;
+
+	pos = murmurhash2(key, strlen(key)) % c->hash->table_size;
+
+	entry = c->hash->entries + pos;
+
+	return entry;
+}
+
+bool cache_get(struct cache *c, const char *key, void **value)
+{
+	pthread_rwlock_rdlock(&c->lock);
+	struct entry *e = entry_for_key(c, key);
+
+	*value = NULL;
+
+	if (e->in_use != IN_USE)
+		goto miss;
+
+	if (strcmp(key, e->key) == 0) {
+		*value = e->value;
+		goto hit;
+	} else {
+		goto miss;
+	}
+
+hit:
+	pthread_rwlock_unlock(&c->lock);
+	return true;
+
+miss:
+	pthread_rwlock_unlock(&c->lock);
+	return false;
+}
+
+
+bool cache_set(struct cache *c, const char *key, void *value)
+{
+	pthread_rwlock_wrlock(&c->lock);
+	struct entry *e = entry_for_key(c, key);
+
+	if (e->in_use == IN_USE)
+		free(e->key);
+
+	e->in_use = IN_USE;
+
+	e->key = strdup(key);
+	e->value = value;
+
+	pthread_rwlock_unlock(&c->lock);
+	return true;
+}
+
+bool cache_del(struct cache *c, const char *key)
+{
+	pthread_rwlock_wrlock(&c->lock);
+	struct entry *e = entry_for_key(c, key);
+
+	if (e->in_use == IN_USE && strcmp(e->key, key) == 0) {
+		free(e->key);
+		e->key = NULL;
+		e->value = NULL;
+		e->in_use = REMOVED;
+		pthread_rwlock_unlock(&c->lock);
+		return true;
+	}
+
+	pthread_rwlock_unlock(&c->lock);
+	return false;
+}
+
diff --git a/libfiu/hash.h b/libfiu/hash.h
new file mode 100644
index 0000000..edd5f6d
--- /dev/null
+++ b/libfiu/hash.h
@@ -0,0 +1,34 @@
+
+#ifndef _HASH_H
+#define _HASH_H
+
+/* Generic hash table. See hash.c for more information. */
+
+#include <sys/types.h>		/* for size_t */
+#include <stdint.h>		/* for int64_t */
+
+typedef struct hash hash_t;
+
+hash_t *hash_create(void (*destructor)(void *));
+void hash_free(hash_t *h);
+
+void *hash_get(hash_t *h, const char *key);
+bool hash_set(hash_t *h, const char *key, void *value);
+bool hash_del(hash_t *h, const char *key);
+
+
+/* Generic cache. */
+
+typedef struct cache cache_t;
+
+cache_t *cache_create();
+bool cache_resize(struct cache *c, size_t new_size);
+void cache_free(cache_t *c);
+
+bool cache_get(cache_t *c, const char *key, void **value);
+bool cache_set(cache_t *c, const char *key, void *value);
+bool cache_del(cache_t *c, const char *key);
+bool cache_invalidate(cache_t *c);
+
+#endif
+
diff --git a/libfiu/wtable.c b/libfiu/wtable.c
new file mode 100644
index 0000000..bf77878
--- /dev/null
+++ b/libfiu/wtable.c
@@ -0,0 +1,358 @@
+
+/*
+ * Wildcard table.
+ *
+ * This is a simple key-value table that associates a key (\0-terminated
+ * string) with a value (void *).
+ *
+ * The keys can end in a wildcard ('*'), and then a lookup will match them as
+ * expected.
+ *
+ * The current implementation is a very simple dynamic array, which is good
+ * enough for a small number of elements (< 100), but we should optimize this
+ * in the future, in particular to speed up lookups.
+ *
+ * If more than one entry matches, we do not guarantee any order, although
+ * this may change in the future.
+ */
+
+#include <sys/types.h>		/* for size_t */
+#include <stdint.h>		/* for [u]int*_t */
+#include <stdbool.h>		/* for bool */
+#include <stdlib.h>		/* for malloc() */
+#include <string.h>		/* for memcpy()/memcmp() */
+#include <stdio.h>		/* snprintf() */
+
+#include "hash.h"
+#include "wtable.h"
+
+
+/* Entry of the wildcard array. */
+struct wentry {
+	char *key;
+	size_t key_len;
+
+	void *value;
+	bool in_use;
+};
+
+struct wtable {
+	/* Final (non-wildcard) entries are kept in this hash. */
+	hash_t *finals;
+
+	/* Wildcarded entries are kept in this dynamic array. */
+	struct wentry *wildcards;
+	size_t ws_size;
+	size_t ws_used_count;
+
+	/* And we keep a cache of lookups into the wildcards array. */
+	cache_t *wcache;
+
+	void (*destructor)(void *);
+};
+
+
+/* Minimum table size. */
+#define MIN_SIZE 10
+
+
+struct wtable *wtable_create(void (*destructor)(void *))
+{
+	struct wtable *t = malloc(sizeof(struct wtable));
+	if (t == NULL)
+		return NULL;
+
+	t->wildcards = NULL;
+	t->wcache = NULL;
+
+	t->finals = hash_create(destructor);
+	if (t->finals == NULL)
+		goto error;
+
+	t->wildcards = malloc(sizeof(struct wentry) * MIN_SIZE);
+	if (t->wildcards == NULL)
+		goto error;
+
+	memset(t->wildcards, 0, sizeof(struct wentry) * MIN_SIZE);
+
+	t->wcache = cache_create();
+	if (t->wcache == NULL)
+		goto error;
+
+	t->ws_size = MIN_SIZE;
+	t->ws_used_count = 0;
+	t->destructor = destructor;
+
+	return t;
+
+error:
+	if (t->finals)
+		hash_free(t->finals);
+	if (t->wcache)
+		cache_free(t->wcache);
+	free(t->wildcards);
+	free(t);
+	return NULL;
+}
+
+void wtable_free(struct wtable *t)
+{
+	int i;
+	struct wentry *entry;
+
+	hash_free(t->finals);
+	cache_free(t->wcache);
+
+	for (i = 0; i < t->ws_size; i++) {
+		entry = t->wildcards + i;
+		if (entry->in_use) {
+			t->destructor(entry->value);
+			free(entry->key);
+		}
+	}
+
+	free(t->wildcards);
+	free(t);
+}
+
+/* Return the last position where s1 and s2 match. */
+static unsigned int strlast(const char *s1, const char *s2)
+{
+	unsigned int i = 0;
+
+	while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) {
+		i++;
+		s1++;
+		s2++;
+	}
+
+	return i;
+}
+
+
+/* True if s is a wildcarded string, False otherwise. */
+static bool is_wildcard(const char *s, size_t len)
+{
+	/* Note we only support wildcards at the end of the string for now. */
+	return s[len - 1] == '*';
+}
+
+/* Checks if ws matches s.
+ *
+ * ws is a "wildcard string", which can end in a '*', in which case we compare
+ * only up to that position, to do a wildcard matching.
+ */
+static bool ws_matches_s(
+		const char *ws, size_t ws_len,
+		const char *s, size_t s_len,
+		bool exact)
+{
+	if (ws == NULL || s == NULL)
+		return false;
+
+	if (exact || !is_wildcard(ws, ws_len)) {
+		/* Exact match */
+		if (ws_len != s_len)
+			return false;
+		return strcmp(ws, s) == 0;
+	} else {
+		/* Inexact match */
+		return strlast(ws, s) >= ws_len - 1;
+	}
+}
+
+/* Find the entry matching the given key.
+ *
+ * If exact == true, then the key must match exactly (no wildcard matching).
+ * If first_free is not NULL, then it will be made to point to the first free
+ * entry found during the lookup.
+ *
+ * Returns a pointer to the entry, or NULL if not found.
+ */
+static struct wentry *wildcards_find_entry(struct wtable *t, const char *key,
+		bool exact, struct wentry **first_free)
+{
+	size_t key_len;
+	size_t pos;
+	struct wentry *entry;
+	bool found_free = false;
+
+	key_len = strlen(key);
+
+	for (pos = 0; pos < t->ws_size; pos++) {
+		entry = t->wildcards + pos;
+		if (!entry->in_use) {
+			if (!found_free && first_free) {
+				*first_free = entry;
+				found_free = true;
+			}
+			continue;
+
+		} else if (ws_matches_s(entry->key, entry->key_len,
+				key, key_len, exact)) {
+			/* The key matches. */
+			return entry;
+		}
+	}
+
+	/* No match. */
+	return NULL;
+}
+
+void *wtable_get(struct wtable *t, const char *key)
+{
+	void *value;
+	struct wentry *entry;
+
+	/* Do an exact lookup first. */
+	value = hash_get(t->finals, key);
+	if (value)
+		return value;
+
+	/* Then see if we can find it in the wcache */
+	if (cache_get(t->wcache, key, &value))
+		return value;
+
+	/* And then walk the wildcards array. */
+	entry = wildcards_find_entry(t, key, false, NULL);
+	if (entry) {
+		cache_set(t->wcache, key, entry->value);
+		return entry->value;
+	}
+
+	/* Cache the negative result as well. */
+	cache_set(t->wcache, key, NULL);
+
+	return NULL;
+}
+
+/* Set on our wildcards table.
+ * For internal use only.
+ * It uses the key as-is (it won't copy it), and it won't resize the array
+ * either. */
+static bool wildcards_set(struct wtable *t, char *key, void *value)
+{
+	struct wentry *entry, *first_free;
+
+	first_free = NULL;
+	entry = wildcards_find_entry(t, key, true, &first_free);
+
+	if (entry) {
+		/* Found a match, override the value. */
+		free(entry->key);
+		entry->key = key;
+		t->destructor(entry->value);
+		entry->value = value;
+		return true;
+	} else if (first_free) {
+		/* We are inserting a new entry. */
+		first_free->key = key;
+		first_free->key_len = strlen(key);
+		first_free->value = value;
+		first_free->in_use = true;
+		t->ws_used_count++;
+		return true;
+	}
+
+	/* This should never happen, as we should always have space by the
+	 * time this function is called. */
+	return false;
+}
+
+static bool resize_table(struct wtable *t, size_t new_size)
+{
+	size_t i;
+	struct wentry *old_wildcards, *e;
+	size_t old_size;
+
+	if (new_size < MIN_SIZE) {
+		/* Do not resize below the minimum size. */
+		return true;
+	}
+
+	old_wildcards = t->wildcards;
+	old_size = t->ws_size;
+
+	t->wildcards = malloc(sizeof(struct wentry) * new_size);
+	if (t->wildcards == NULL)
+		return false;
+
+	memset(t->wildcards, 0, sizeof(struct wentry) * new_size);
+	t->ws_size = new_size;
+	t->ws_used_count = 0;
+
+	/* Insert the old entries into the new table. */
+	for (i = 0; i < old_size; i++) {
+		e = old_wildcards + i;
+		if (e->in_use) {
+			wildcards_set(t, e->key, e->value);
+		}
+	}
+
+	free(old_wildcards);
+
+	/* Keep the cache the same size as our table, which works reasonably
+	 * well in practise */
+	cache_resize(t->wcache, new_size);
+
+	return true;
+}
+
+bool wtable_set(struct wtable *t, const char *key, void *value)
+{
+	if (is_wildcard(key, strlen(key))) {
+		if ((t->ws_size - t->ws_used_count) <= 1) {
+			/* If we only have one entry left, grow by 30%, plus
+			 * one to make sure we always increase even if the
+			 * percentage isn't enough. */
+			if (!resize_table(t, t->ws_size * 1.3 + 1))
+				return false;
+		}
+
+		return wildcards_set(t, strdup(key), value);
+	} else {
+		return hash_set(t->finals, key, value);
+	}
+}
+
+
+bool wtable_del(struct wtable *t, const char *key)
+{
+	struct wentry *entry;
+
+	if (is_wildcard(key, strlen(key))) {
+		entry = wildcards_find_entry(t, key, true, NULL);
+
+		if (!entry) {
+			/* Key not found. */
+			return false;
+		}
+
+		/* Mark the entry as free. */
+		free(entry->key);
+		entry->key = NULL;
+		entry->key_len = 0;
+		t->destructor(entry->value);
+		entry->value = NULL;
+		entry->in_use = false;
+
+		t->ws_used_count--;
+
+		/* Shrink if the table is less than 60% occupied. */
+		if (t->ws_size > MIN_SIZE &&
+				(float) t->ws_used_count / t->ws_size < 0.6) {
+			if (!resize_table(t, t->ws_used_count + 3))
+				return false;
+		}
+
+		/* Invalidate the cache. We could be smart and walk it,
+		 * removing only the positive hits, but it's also more
+		 * expensive */
+		cache_invalidate(t->wcache);
+
+		return true;
+	} else {
+		return hash_del(t->finals, key);
+	}
+}
+
diff --git a/libfiu/wtable.h b/libfiu/wtable.h
new file mode 100644
index 0000000..e39f6c5
--- /dev/null
+++ b/libfiu/wtable.h
@@ -0,0 +1,24 @@
+
+/* Wildcard table.
+ *
+ * This is a simple key-value table that associates a key (\0-terminated
+ * string) with a value (void *).
+ *
+ * See wtable.c for more information. */
+
+#ifndef _WTABLE_H
+#define _WTABLE_H
+
+#include <stdbool.h>		/* for bool */
+
+typedef struct wtable wtable_t;
+
+wtable_t *wtable_create(void (*destructor)(void *));
+void wtable_free(wtable_t *t);
+
+void *wtable_get(wtable_t *t, const char *key);
+bool wtable_set(wtable_t *t, const char *key, void *value);
+bool wtable_del(wtable_t *t, const char *key);
+
+#endif
+
diff --git a/tests/Makefile b/tests/Makefile
index 41d63d3..cdc478d 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -38,7 +38,7 @@ C_BINS := $(patsubst %.c,%,$(C_SRCS))
 c-tests: $(patsubst %.c,c-run-%,$(C_SRCS))
 
 test-%: test-%.o build-flags
-	$(NICE_CC) $(ALL_CFLAGS) $< -lfiu -o $@
+	$(NICE_CC) $(ALL_CFLAGS) $< -lfiu -lpthread -o $@
 
 c-run-%: %
 	$(NICE_RUN) ./$<
diff --git a/tests/perf-fsck.py b/tests/perf-fsck.py
new file mode 100644
index 0000000..8d367b9
--- /dev/null
+++ b/tests/perf-fsck.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python
+
+"""
+Performance tests using fsck.ext2 on a test file.
+
+It can be tuned with the following environment variables:
+
+ - TEST_FILE: The name of the file used for testing. By default, ".test_fs".
+ - TEST_FILE_SIZE_MB: The size of the test file, in megabytes. Only used if
+   the file doesn't exist. Default: 10.
+ - VERBOSE: Show verbose output (from 0 to 2). Default: 0.
+ - LD_LIBRARY_PATH: Library path to pass on to the subcommands.
+   Default: <directory of this file>/../libfiu/, which is usually the one
+   containing the current build.
+"""
+
+import os
+import sys
+import subprocess
+import time
+
+test_file = os.environ.get('TEST_FILE', '.test_fs')
+
+test_file_size = int(os.environ.get('TEST_FILE_SIZE_MB', 10))
+
+ld_library_path = os.environ.get('LD_LIBRARY_PATH',
+        os.path.abspath(
+            os.path.abspath(os.path.dirname(sys.argv[0]))
+            + '/../libfiu/'))
+
+verbose = int(os.environ.get('VERBOSE', 0))
+
+dev_null = open('/dev/null', 'w')
+
+def run_child(name, args, stdout = dev_null, stderr = dev_null):
+    """Runs the subprocess, returns the Popen object."""
+    env = dict(os.environ)
+    env['LD_LIBRARY_PATH'] = ld_library_path
+
+    if verbose and stdout == stderr == dev_null:
+        stdout = stderr = None
+
+    child = subprocess.Popen(args, env = env,
+            stdout = stdout, stderr = stderr)
+    return child
+
+def run_and_time(name, args):
+    """Run the given arguments, print the times."""
+    start = time.time()
+    child = run_child(name, args)
+    _, status, rusage = os.wait4(child.pid, 0)
+    end = time.time()
+
+    if verbose == 2:
+        print 'Ran %s -> %d' % (args, status)
+
+    if status != 0:
+        print 'Error running %s: %s' % (args[0], status)
+        raise RuntimeError
+
+    print '%-10s u:%.3f  s:%.3f  r:%.3f' % (
+            name, rusage.ru_utime, rusage.ru_stime, end - start)
+
+
+def run_fsck(name, fiu_args):
+    """Runs an fsck with the given fiu arguments."""
+    child = run_child(name,
+            ["fiu-run", "-x"] + fiu_args
+                + "fsck.ext2 -n -t -f".split() + [test_file],
+                stdout = subprocess.PIPE,
+                stderr = subprocess.PIPE)
+    stdout, stderr = child.communicate()
+
+    if child.returncode != 0:
+        print 'Error running fsck: %s' % child.returncode
+        raise RuntimeError
+
+    # Find the times reported by fsck.
+    # Not very robust, but useful as it measures the real program time run,
+    # and not the startup overhead.
+    # The line looks like:
+    #   Memory used: 560k/0k (387k/174k), time:  2.18/ 2.17/ 0.00
+    user_time = sys_time = real_time = -1
+    for l in stdout.split('\n'):
+        if not l.startswith("Memory used"):
+            continue
+
+        times = l.split(':')[-1].split('/')
+        times = [s.strip() for s in times]
+        if len(times) != 3:
+            continue
+
+        real_time = float(times[0])
+        user_time = float(times[1])
+        sys_time = float(times[2])
+        break
+
+    print '%-10s u:%.3f  s:%.3f  r:%.3f' % (
+            name, user_time, sys_time, real_time)
+
+
+def check_test_file():
+    if os.path.exists(test_file):
+        return
+
+    with open(test_file, 'w') as fd:
+        fd.truncate(test_file_size * 1024 * 1024)
+
+    retcode = subprocess.call(
+            ["mkfs.ext2", "-F", test_file],
+            stdout = open('/dev/null', 'w'))
+    if retcode != 0:
+        print 'Error running mkfs.ext2:', retcode
+        return
+
+
+if __name__ == '__main__':
+    check_test_file()
+
+    run_and_time("base", "fsck.ext2 -n -f".split() + [test_file])
+
+    # 1 all-matching wildcard.
+    run_fsck("w1", ["-c", "enable_random name=*,probability=0"])
+
+    # 1k final failure points, no matches.
+    args = []
+    for i in range(1000):
+        args += ["-c", "enable_random name=none/%d,probability=0" % i]
+    run_fsck("f1k", args)
+
+    # 1k wildcard failure points, no matches.
+    args = []
+    for i in range(1000):
+        args += ["-c", "enable_random name=none/%d/*,probability=0" % i]
+    run_fsck("w1k", args)
+
+    # 1k wildcarded failure points, and 1 match.
+    args = []
+    for i in range(1000):
+        args += ["-c", "enable_random name=none/%d/*,probability=0" % i]
+    args += ["-c", "enable_random name=*,probability=0"]
+    run_fsck("w1k+1", args)
+
+    # 1k final failure points, *all* matches.
+    args = []
+    for i in range(1000):
+        args += ["-c", "enable_random name=*,probability=0"]
+    run_fsck("m1k", args)
diff --git a/tests/test-manyfps.py b/tests/test-manyfps.py
new file mode 100644
index 0000000..5679993
--- /dev/null
+++ b/tests/test-manyfps.py
@@ -0,0 +1,25 @@
+
+"""
+Test the creation of many failure points.
+"""
+
+import fiu
+
+N = 1000
+
+for i in range(N):
+    fiu.enable(str(i))
+
+for i in range(N):
+    assert fiu.fail(str(i))
+
+# Remove only half and check again; this will stress the shrinking of our data
+# structures.
+for i in range(N / 2):
+    fiu.disable(str(i))
+
+for i in range(N / 2, N):
+    assert fiu.fail(str(i))
+
+for i in range(N / 2, N):
+    fiu.disable(str(i))
diff --git a/tests/test-onetime.py b/tests/test-onetime.py
new file mode 100644
index 0000000..182a4a8
--- /dev/null
+++ b/tests/test-onetime.py
@@ -0,0 +1,17 @@
+
+"""
+Test that we fail ONETIME points only one time.
+"""
+
+import fiu
+
+fiu.enable('p1', flags = fiu.Flags.ONETIME)
+fiu.enable('p2')
+
+assert fiu.fail('p1')
+for i in range(100):
+    assert not fiu.fail('p1')
+
+for i in range(100):
+    assert fiu.fail('p2')
+
diff --git a/tests/test-parallel-wildcard.c b/tests/test-parallel-wildcard.c
new file mode 100644
index 0000000..3aaa136
--- /dev/null
+++ b/tests/test-parallel-wildcard.c
@@ -0,0 +1,211 @@
+
+/* Test creation and removal of wildcarded failure points while checking them
+ * on a different thread.
+ *
+ * This test will have a thread enabling and disabling failure points like
+ * point:number:1:*, point:number:2:*, ...
+ *
+ * Then it has two threads checking:
+ * point:number:1:1, point:number:1:2, ...,
+ * point:number:2:1, point:number:2:2, ...
+ *
+ * Please note this is a non-deterministic test. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <assert.h>
+
+#include <fiu.h>
+#include <fiu-control.h>
+
+/* Be careful with increasing these numbers, as the memory usage is directly
+ * related to them. */
+#define NHIGH 1000
+#define NLOW  1000
+
+/* Maximum number of a failure point's name */
+#define MAX_FPNAME 32
+
+/* How many seconds to run the test */
+#define TEST_TIME 5
+
+/* Signal for the threads to stop. */
+bool stop_threads = false;
+
+/* The name of each final point. */
+char final_point_name[NHIGH][NLOW][MAX_FPNAME];
+
+/* The name of each wildcarded point. */
+char wildcard_point_name[NHIGH][MAX_FPNAME];
+
+char *make_final_point_name(char *name, int high, int low)
+{
+	sprintf(name, "point/number/%d/%d", high, low);
+	return name;
+}
+
+char *make_wildcard_point_name(char *name, int high)
+{
+	sprintf(name, "point/number/%d/*", high);
+	return name;
+}
+
+/* Calls all the final points all the time. */
+unsigned long long no_check_caller_count = 0;
+void *no_check_caller(void *unused)
+{
+	int high, low;
+
+	for (;;) {
+		for (high = 0; high < NHIGH; high++) {
+			for (low = 0; low < NLOW; low++) {
+				fiu_fail(final_point_name[high][low]);
+				no_check_caller_count++;
+			}
+			if (stop_threads)
+				return NULL;
+		}
+	}
+
+	return NULL;
+}
+
+bool rand_bool(void) {
+	return (rand() % 2) == 0;
+}
+
+/* Used too know if a point is enabled or not. */
+bool enabled[NHIGH];
+pthread_rwlock_t enabled_lock;
+
+
+/* Calls all the *enabled* points all the time. */
+unsigned long long checking_caller_count = 0;
+void *checking_caller(void *unused)
+{
+	int high, low;
+	int failed;
+
+	for (;;) {
+		for (high = 0; high < NHIGH; high++) {
+			pthread_rwlock_rdlock(&enabled_lock);
+			if (!enabled[high]) {
+				pthread_rwlock_unlock(&enabled_lock);
+				continue;
+			}
+
+			for (low = 0; low < NLOW; low++) {
+				failed = fiu_fail(
+					final_point_name[high][low]) != 0;
+				if (!failed) {
+					printf("ERROR: %d:%d did not fail\n",
+							high, low);
+					assert(false);
+				}
+				checking_caller_count++;
+			}
+			pthread_rwlock_unlock(&enabled_lock);
+
+			if (stop_threads)
+				return NULL;
+		}
+	}
+
+	return NULL;
+}
+
+/* Enable and disable wildcarded points all the time. */
+unsigned long long enabler_count = 0;
+void *enabler(void *unused)
+{
+	int high;
+
+	for (;;) {
+		for (high = 0; high < NHIGH; high++) {
+			/* 50% chance of flipping this point. */
+			if (rand_bool())
+				continue;
+
+			pthread_rwlock_wrlock(&enabled_lock);
+			if (enabled[high]) {
+				assert(fiu_disable(wildcard_point_name[high])
+						== 0);
+				enabled[high] = false;
+			} else {
+				assert(
+					fiu_enable(
+						wildcard_point_name[high],
+						1, NULL, 0) == 0);
+				enabled[high] = true;
+			}
+			pthread_rwlock_unlock(&enabled_lock);
+			enabler_count++;
+
+			if (stop_threads)
+				return NULL;
+		}
+	}
+
+	return NULL;
+}
+
+void disable_all()
+{
+	int high;
+
+	pthread_rwlock_wrlock(&enabled_lock);
+	for (high = 0; high < NHIGH; high++) {
+		/* Note this could fail as we don't check if they're active or
+		 * not, but here we don't care. */
+		fiu_disable(wildcard_point_name[high]);
+	}
+	pthread_rwlock_unlock(&enabled_lock);
+}
+
+int main(void)
+{
+	int high, low;
+
+	pthread_t t1, t2, t3;
+
+	fiu_init(0);
+
+	for (high = 0; high < NHIGH; high++) {
+		make_wildcard_point_name(wildcard_point_name[high], high);
+
+		for (low = 0; low < NLOW; low++) {
+			make_final_point_name(final_point_name[high][low],
+					high, low);
+		}
+	}
+
+	pthread_rwlock_init(&enabled_lock, NULL);
+
+	pthread_create(&t1, NULL, no_check_caller, NULL);
+	pthread_create(&t2, NULL, checking_caller, NULL);
+	pthread_create(&t3, NULL, enabler, NULL);
+
+	sleep(TEST_TIME);
+
+	stop_threads = 1;
+
+	pthread_join(t1, NULL);
+	pthread_join(t2, NULL);
+	pthread_join(t3, NULL);
+
+	disable_all();
+
+	pthread_rwlock_destroy(&enabled_lock);
+
+	printf("wildcard nc: %-8llu  c: %-8llu  e: %-8llu  t: %llu\n",
+			no_check_caller_count, checking_caller_count,
+			enabler_count,
+			no_check_caller_count + checking_caller_count
+				+ enabler_count);
+
+	return 0;
+}
+
diff --git a/tests/test-parallel.c b/tests/test-parallel.c
new file mode 100644
index 0000000..59cb315
--- /dev/null
+++ b/tests/test-parallel.c
@@ -0,0 +1,176 @@
+
+/* Test creation and removal of failure points while checking them on a
+ * different thread.
+ *
+ * Please note this is a non-deterministic test. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <assert.h>
+
+#include <fiu.h>
+#include <fiu-control.h>
+
+#define NPOINTS 10000
+
+/* How many seconds to run the test */
+#define TEST_TIME 5
+
+/* Signal for the threads to stop. */
+bool stop_threads = false;
+
+/* The name of each point */
+char point_name[NPOINTS][16];
+
+char *make_point_name(char *name, int i)
+{
+	sprintf(name, "fp-%d", i);
+	return name;
+}
+
+/* Calls all the points all the time. */
+unsigned long long no_check_caller_count = 0;
+void *no_check_caller(void *unused)
+{
+	int i = 0;
+
+	while (!stop_threads) {
+		fiu_fail(point_name[i]);
+
+		i = (i + 1) % NPOINTS;
+
+		if (i == 0)
+			no_check_caller_count++;
+	}
+
+	no_check_caller_count = no_check_caller_count * NPOINTS + i;
+
+	return NULL;
+}
+
+bool rand_bool(void) {
+	return (rand() % 2) == 0;
+}
+
+/* Used too know if a point is enabled or not. */
+bool enabled[NPOINTS];
+pthread_rwlock_t enabled_lock;
+
+
+/* Calls all the *enabled* points all the time. */
+unsigned long long checking_caller_count = 0;
+void *checking_caller(void *unused)
+{
+	int i = 0;
+	int failed;
+
+	while (!stop_threads) {
+		pthread_rwlock_rdlock(&enabled_lock);
+		if (enabled[i]) {
+			failed = fiu_fail(point_name[i]) != 0;
+			pthread_rwlock_unlock(&enabled_lock);
+
+			if (!failed) {
+				printf("ERROR: fp %d did not fail\n", i);
+				assert(false);
+			}
+		} else {
+			pthread_rwlock_unlock(&enabled_lock);
+		}
+
+		i = (i + 1) % NPOINTS;
+
+		if (i == 0)
+			checking_caller_count++;
+	}
+
+	checking_caller_count = checking_caller_count * NPOINTS + i;
+
+	return NULL;
+}
+
+/* Enable and disable points all the time. */
+unsigned long long enabler_count = 0;
+void *enabler(void *unused)
+{
+	int i = 0;
+
+	while (!stop_threads) {
+		if (rand_bool()) {
+			pthread_rwlock_wrlock(&enabled_lock);
+			if (enabled[i]) {
+				assert(fiu_disable(point_name[i]) == 0);
+				enabled[i] = false;
+			} else {
+				assert(fiu_enable(point_name[i], 1, NULL, 0)
+						== 0);
+				enabled[i] = true;
+			}
+			pthread_rwlock_unlock(&enabled_lock);
+		}
+
+		i = (i + 1) % NPOINTS;
+
+		if (i == 0)
+			enabler_count++;
+	}
+
+	enabler_count = enabler_count * NPOINTS + i;
+
+	return NULL;
+}
+
+void disable_all()
+{
+	int i = 0;
+
+	pthread_rwlock_wrlock(&enabled_lock);
+	for (i = 0; i < NPOINTS; i++) {
+		/* Note this could fail as we don't check if they're active or
+		 * not, but here we don't care. */
+		fiu_disable(point_name[i]);
+	}
+	pthread_rwlock_unlock(&enabled_lock);
+}
+
+int main(void)
+{
+	int i;
+
+	pthread_t t1, t2, t3;
+
+	fiu_init(0);
+
+	for (i = 0; i < NPOINTS; i++)
+		make_point_name(point_name[i], i);
+
+	pthread_rwlock_init(&enabled_lock, NULL);
+
+	pthread_create(&t1, NULL, no_check_caller, NULL);
+	pthread_create(&t2, NULL, checking_caller, NULL);
+	pthread_create(&t3, NULL, enabler, NULL);
+
+	sleep(TEST_TIME);
+
+	stop_threads = 1;
+
+	pthread_join(t1, NULL);
+	pthread_join(t2, NULL);
+	pthread_join(t3, NULL);
+
+	disable_all();
+
+	pthread_rwlock_destroy(&enabled_lock);
+
+	printf("parallel nc: %-8llu  c: %-8llu  e: %-8llu  t: %llu\n",
+			no_check_caller_count, checking_caller_count,
+			enabler_count,
+			no_check_caller_count + checking_caller_count
+				+ enabler_count);
+
+	return 0;
+}
+
diff --git a/tests/test-wildcards.py b/tests/test-wildcards.py
new file mode 100644
index 0000000..6603299
--- /dev/null
+++ b/tests/test-wildcards.py
@@ -0,0 +1,44 @@
+"""
+Test the behaviour of the wildcard failure points.
+"""
+
+import fiu
+
+fiu.enable("a:b:c")
+assert fiu.fail("a:b:c")
+
+fiu.enable("a:b:*")
+assert fiu.fail("a:b:c")
+assert fiu.fail("a:b:x")
+assert fiu.fail("a:b:c:d")
+
+fiu.enable("a:b:c:d")
+assert fiu.fail("a:b:c:d")
+
+fiu.disable("a:b:c")
+assert fiu.fail("a:b:c")
+
+fiu.disable("a:b:*")
+assert not fiu.fail("a:b:c")
+assert not fiu.fail("a:b:x")
+assert fiu.fail("a:b:c:d")
+
+fiu.disable("a:b:c:d")
+assert not fiu.fail("a:b:c:d")
+
+
+s = "x"
+for i in range(200):
+    fiu.enable(s + "/*")
+    s += "/x"
+
+s = "x"
+for i in range(200):
+    assert fiu.fail(s + '/asdf')
+    fiu.disable(s + "/*")
+    s += "/x"
+
+fiu.enable("*")
+assert fiu.fail("asdf")
+fiu.disable("*")
+assert not fiu.fail("asdf")
diff --git a/utils/fiu-ctrl b/utils/fiu-ctrl
index 140bb37..1b400d8 100755
--- a/utils/fiu-ctrl
+++ b/utils/fiu-ctrl
@@ -40,7 +40,7 @@ All of the enable\* can also optionally take 'failnum' and 'failinfo'
 parameters, analogous to the ones taken by the C functions.
 
 The following options existed in the past but are deprecated and WILL BE
-REMOVED in future releases: -e, -p, -u and -i.
+REMOVED in future releases: -e, -p, -u, -i, and -d.
 
 
 Examples:
@@ -80,13 +80,13 @@ function opts_reset() {
 }
 
 function add_deprecated_enable() {
-	if [ "$NAME" == "" ]; then
+	if [ "$DEP_NAME" == "" ]; then
 		return
 	fi;
 
 	PARAMS="name=$DEP_NAME,failnum=$DEP_FAILNUM,failinfo=$DEP_FAILINFO"
-	if [ $PROB -ge 0 ]; then
-		C="enable_random $PARAMS,probability=$PROB"
+	if [ "$DEP_PROB" -ge 0 ]; then
+		C="enable_random $PARAMS,probability=0.$DEP_PROB"
 	else
 		C="enable $PARAMS"
 	fi
@@ -94,6 +94,10 @@ function add_deprecated_enable() {
 	CMDS[${#CMDS[*]}]="$C"
 }
 
+function deprecated_warning() {
+	echo "Warning: this option is deprecated and will be removed." >&2
+}
+
 opts_reset;
 while getopts "+c:e:p:u:i:d:f:h" opt; do
 	case $opt in
@@ -102,23 +106,28 @@ while getopts "+c:e:p:u:i:d:f:h" opt; do
 		;;
 	e)
 		# add the current one, if any
+		deprecated_warning;
 		add_deprecated_enable;
 		opts_reset;
 		DEP_NAME="$OPTARG"
 		;;
 	p)
+		deprecated_warning;
 		DEP_PROB="$OPTARG"
 		;;
 	u)
+		deprecated_warning;
 		DEP_FAILNUM="$OPTARG"
 		;;
 	i)
+		deprecated_warning;
 		DEP_FAILINFO="$OPTARG"
 		;;
 	f)
 		FIFO_PREFIX="$OPTARG"
 		;;
 	d)
+		deprecated_warning;
 		CMDS[${#CMDS[*]}]="disable name=$OPTARG"
 		opts_reset;
 		;;
@@ -130,8 +139,8 @@ while getopts "+c:e:p:u:i:d:f:h" opt; do
 done
 
 # add leftovers
-if [ "$NAME" != "" ]; then
-	add_cmd;
+if [ "$DEP_NAME" != "" ]; then
+	add_deprecated_enable;
 fi
 
 # eat the parameters we already processed
@@ -167,6 +176,7 @@ function send_cmd_fifo() {
 	if [ "$REPLY" == "-1" ]; then
 		echo "$P: Command '$@' returned error ($REPLY)"
 	fi
+	#echo "$P: $@ -> $REPLY"
 }
 
 for c in "${CMDS[@]}"; do
