]> git.ipfire.org Git - thirdparty/bird.git/commitdiff
Fixed a bug in hot page global storage
authorMaria Matejka <mq@ucw.cz>
Fri, 5 May 2023 07:39:13 +0000 (09:39 +0200)
committerMaria Matejka <mq@ucw.cz>
Sat, 6 May 2023 08:50:32 +0000 (10:50 +0200)
The original algorithm was suffering from an ABA race condition:

A: fp = page_stack
B: completely allocates the same page and writes into it some data
A: unsuspecting, loads (invalid) next = fp->next
B: finishes working with the page and returns it back to page_stack
A: compare-exchange page_stack: fp => next succeeds and writes garbage
to page_stack

Fixed this by using an implicit spinlock in hot page allocator.

lib/birdlib.h
sysdep/unix/Makefile
sysdep/unix/alloc.c
sysdep/unix/alloc_test.c [new file with mode: 0644]
sysdep/unix/log.c

index 9132fb93f03fe8fe906f12a6a52199684b9f4d10..2eec5c0f15b9cc0b2e11cebcd5831e078330a502 100644 (file)
@@ -183,7 +183,11 @@ void bug(const char *msg, ...) NORET;
 void debug(const char *msg, ...);      /* Printf to debug output */
 void debug_safe(const char *msg);      /* Printf to debug output, async-safe */
 
+/* Internal thread ID, useful for logging */
+extern _Atomic uint max_thread_id;
 extern _Thread_local uint this_thread_id;
+#define THIS_THREAD_ID  (this_thread_id ?: (this_thread_id = atomic_fetch_add_explicit(&max_thread_id, 1, memory_order_acq_rel)))
+
 
 /* Debugging */
 
index 6f6b0d267f9fda210e9fa648a7b49e0ade8000b3..efbf3c7090cf0bafbc6a8ce4892fcd8fb580813a 100644 (file)
@@ -5,4 +5,7 @@ $(cf-local)
 $(conf-y-targets): $(s)krt.Y
 
 src := $(filter-out main.c, $(src))
+
+tests_src := alloc_test.c
+tests_targets := $(tests_targets) $(tests-target-files)
 tests_objs := $(tests_objs) $(src-o-files)
index b7566f96fc5f35c4d6e673dbeebdabddfecfde01..56e755db2d4aacdcf039aeae977a36439d6e100d 100644 (file)
@@ -10,6 +10,7 @@
 #include "lib/resource.h"
 #include "lib/lists.h"
 #include "lib/event.h"
+#include "lib/io-loop.h"
 
 #include <errno.h>
 #include <stdlib.h>
@@ -91,7 +92,7 @@ long page_size = 0;
        .next = next,
        .pos = pos,
        .type = type,
-       .thread_id = this_thread_id,
+       .thread_id = THIS_THREAD_ID,
       };
     }
 
@@ -104,12 +105,12 @@ long page_size = 0;
 #   define ajlog(...)
 
     struct free_page {
-      struct free_page * _Atomic next;
+      struct free_page *next;
     };
 
 # endif
 
-# define WRITE_NEXT(pg, val)   do { UNPROTECT_PAGE((pg)); atomic_store_explicit(&(pg)->next, (val), memory_order_release); PROTECT_PAGE((pg)); } while (0)
+# define WRITE_NEXT(pg, val)   do { UNPROTECT_PAGE((pg)); (pg)->next = (val); PROTECT_PAGE((pg)); } while (0)
 
 # define EP_POS_MAX    ((page_size - OFFSETOF(struct empty_pages, pages)) / sizeof (void *))
 
@@ -125,6 +126,15 @@ long page_size = 0;
 
   static struct free_page * _Atomic page_stack = NULL;
   static _Thread_local struct free_page * local_page_stack = NULL;
+  static struct free_page page_stack_blocked;
+
+  /* Try to replace the page stack head with a cork, until it succeeds. */
+# define PAGE_STACK_GET        ({ \
+    struct free_page *fp; \
+    while ((fp = atomic_exchange_explicit(&page_stack, &page_stack_blocked, memory_order_acq_rel)) == &page_stack_blocked) birdloop_yield(); \
+    fp; })
+  /* Reinstate the stack with another value */
+# define PAGE_STACK_PUT(val)   ASSERT_DIE(atomic_exchange_explicit(&page_stack, (val), memory_order_acq_rel) == &page_stack_blocked)
 
   static void page_cleanup(void *);
   static event page_cleanup_event = { .hook = page_cleanup, };
@@ -171,7 +181,7 @@ alloc_page(void)
   struct free_page *fp = local_page_stack;
   if (fp)
   {
-    local_page_stack = atomic_load_explicit(&fp->next, memory_order_acquire);
+    local_page_stack = fp->next;
     atomic_fetch_sub_explicit(&pages_kept_locally, 1, memory_order_relaxed);
     pages_kept_here--;
     UNPROTECT_PAGE(fp);
@@ -182,20 +192,23 @@ alloc_page(void)
   ASSERT_DIE(pages_kept_here == 0);
 
   /* If there is any free page kept hot in global storage, we use it. */
-  fp = atomic_load_explicit(&page_stack, memory_order_acquire);
-  while (fp && !atomic_compare_exchange_strong_explicit(
-       &page_stack, &fp, atomic_load_explicit(&fp->next, memory_order_acquire),
-       memory_order_acq_rel, memory_order_acquire))
-    ;
-
-  if (fp)
+  if (fp = PAGE_STACK_GET)
   {
-    uint pk = atomic_fetch_sub_explicit(&pages_kept, 1, memory_order_relaxed);
+    /* Reinstate the stack with the next page in list */
+    PAGE_STACK_PUT(fp->next);
+
+    /* Update the counters */
+    UNUSED uint pk = atomic_fetch_sub_explicit(&pages_kept, 1, memory_order_relaxed);
+
+    /* Release the page */
     UNPROTECT_PAGE(fp);
-    ajlog(fp, atomic_load_explicit(&fp->next, memory_order_relaxed), pk, AJT_ALLOC_GLOBAL_HOT);
+    ajlog(fp, fp->next, pk, AJT_ALLOC_GLOBAL_HOT);
     return fp;
   }
 
+  /* Reinstate the stack with zero */
+  PAGE_STACK_PUT(NULL);
+
   /* If there is any free page kept cold, we use that. */
   LOCK_DOMAIN(resource, empty_pages_domain);
   if (empty_pages) {
@@ -247,8 +260,7 @@ free_page(void *ptr)
   struct free_page *fp = ptr;
   if (shutting_down || (pages_kept_here < KEEP_PAGES_MAX_LOCAL))
   {
-    struct free_page *next = local_page_stack;
-    atomic_store_explicit(&fp->next, next, memory_order_relaxed);
+    UNUSED struct free_page *next = fp->next = local_page_stack;
     PROTECT_PAGE(fp);
     local_page_stack = fp;
 
@@ -259,16 +271,13 @@ free_page(void *ptr)
   }
 
   /* If there are too many local pages, we add the free page to the global hot-free-page list */
-  struct free_page *next = atomic_load_explicit(&page_stack, memory_order_acquire);
-
-  atomic_store_explicit(&fp->next, next, memory_order_release);
+  UNUSED struct free_page *next = fp->next = PAGE_STACK_GET;
   PROTECT_PAGE(fp);
 
-  while (!atomic_compare_exchange_strong_explicit(
-       &page_stack, &next, fp,
-       memory_order_acq_rel, memory_order_acquire))
-    WRITE_NEXT(fp, next);
+  /* Unblock the stack with the page being freed */
+  PAGE_STACK_PUT(fp);
 
+  /* Update counters */
   uint pk = atomic_fetch_add_explicit(&pages_kept, 1, memory_order_relaxed);
   ajlog(fp, next, pk, AJT_FREE_GLOBAL_HOT);
 
@@ -292,7 +301,7 @@ flush_local_pages(void)
    * Also, we need to know the last page. */
   struct free_page *last = local_page_stack, *next;
   int check_count = 1;
-  while (next = atomic_load_explicit(&last->next, memory_order_acquire))
+  while (next = last->next)
   {
     check_count++;
     last = next;
@@ -301,15 +310,13 @@ flush_local_pages(void)
   /* The actual number of pages must be equal to the counter value. */
   ASSERT_DIE(check_count == pages_kept_here);
 
-  /* Repeatedly trying to insert the whole page list into global page stack at once. */
-  next = atomic_load_explicit(&page_stack, memory_order_acquire);
+  /* Block the stack by a cork */
+  UNPROTECT_PAGE(last);
+  last->next = PAGE_STACK_GET;
+  PROTECT_PAGE(last);
 
-  /* First we set the outwards pointer (from our last),
-   * then we try to set the inwards pointer to our first page. */
-  do WRITE_NEXT(last, next);
-  while (!atomic_compare_exchange_strong_explicit(
-       &page_stack, &next, local_page_stack,
-       memory_order_acq_rel, memory_order_acquire));
+  /* Update the stack */
+  PAGE_STACK_PUT(last);
 
   /* Finished. Now the local stack is empty. */
   local_page_stack = NULL;
@@ -333,7 +340,12 @@ page_cleanup(void *_ UNUSED)
 
   ajlog(NULL, NULL, 0, AJT_CLEANUP_BEGIN);
 
-  struct free_page *stack = atomic_exchange_explicit(&page_stack, NULL, memory_order_acq_rel);
+  /* Prevent contention */
+  struct free_page *stack = PAGE_STACK_GET;
+
+  /* Always replace by zero */
+  PAGE_STACK_PUT(NULL);
+
   if (!stack)
   {
     ajlog(NULL, NULL, 0, AJT_CLEANUP_NOTHING);
@@ -342,7 +354,7 @@ page_cleanup(void *_ UNUSED)
 
   do {
     struct free_page *fp = stack;
-    stack = atomic_load_explicit(&fp->next, memory_order_acquire);
+    stack = fp->next;
 
     LOCK_DOMAIN(resource, empty_pages_domain);
     /* Empty pages are stored as pointers. To store them, we need a pointer block. */
@@ -384,7 +396,7 @@ page_cleanup(void *_ UNUSED)
   while (stack)
   {
     struct free_page *f = stack;
-    stack = atomic_load_explicit(&f->next, memory_order_acquire);
+    stack = f->next;
     UNPROTECT_PAGE(f);
     free_page(f);
 
diff --git a/sysdep/unix/alloc_test.c b/sysdep/unix/alloc_test.c
new file mode 100644 (file)
index 0000000..202af2b
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ *     BIRD -- Allocator Tests
+ *
+ *     (c) 2023       CZ.NIC z.s.p.o.
+ *     (c) 2023       Maria Matejka <mq@jmq.cz>
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#include "test/birdtest.h"
+#include "test/bt-utils.h"
+#include "lib/resource.h"
+
+#include <unistd.h>
+#include <pthread.h>
+
+#define ALLOC_AND_TREE_LIMIT   (1 << 14)
+
+static void *
+alloc_and_free_main(void *data UNUSED)
+{
+#define BLOCK_SIZE     32
+  void *block[BLOCK_SIZE];
+
+  for (int i=0; i<ALLOC_AND_TREE_LIMIT; i++)
+  {
+    for (int b=0; b<BLOCK_SIZE; b++)
+    {
+      block[b] = alloc_page();
+      ASSERT_DIE(PAGE_HEAD(block[b]) == block[b]);
+      memset(block[b], 0x42, page_size);
+    }
+
+    for (int b=0; b<BLOCK_SIZE; b++)
+    {
+      free_page(block[b]);
+      block[b] = alloc_page();
+      ASSERT_DIE(PAGE_HEAD(block[b]) == block[b]);
+      memset(block[b], 0x53, page_size); 
+    }
+
+    for (int b=0; b<BLOCK_SIZE; b++)
+      free_page(block[b]);
+  }
+
+  return NULL;
+}
+
+static int
+t_alloc_and_free(void)
+{
+#define THR_N  16
+  pthread_t tid[THR_N];
+
+  for (int i=0; i<THR_N; i++)
+  {
+    pthread_create(&tid[i], NULL, alloc_and_free_main, NULL);
+    usleep(50 * i);
+  }
+
+  for (int i=0; i<THR_N; i++)
+    pthread_join(tid[i], NULL);
+
+  return 1;
+}
+
+int main(int argc, char **argv)
+{
+  bt_init(argc, argv);
+  
+  bt_test_suite(t_alloc_and_free, "Testing parallel allocations and free");
+
+  return bt_exit_value();
+}
index c200db38ffe81e075e68228852d9ea09ad4c8eed..d929e80ee86ad03c606aa3efec2db9aad56de822 100644 (file)
@@ -37,11 +37,9 @@ static FILE *dbgf;
 static list *current_log_list;
 static char *current_syslog_name; /* NULL -> syslog closed */
 
-static _Atomic uint max_thread_id = ATOMIC_VAR_INIT(1);
+_Atomic uint max_thread_id = ATOMIC_VAR_INIT(1);
 _Thread_local uint this_thread_id;
 
-#define THIS_THREAD_ID  (this_thread_id ?: (this_thread_id = atomic_fetch_add_explicit(&max_thread_id, 1, memory_order_acq_rel)))
-
 #include <pthread.h>
 
 static pthread_mutex_t log_mutex;