diff --git a/openair2/PHY_INTERFACE/.gitignore b/openair2/PHY_INTERFACE/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2dca7a7611a5ca3548d6f4cd2d35b0c063402adb
--- /dev/null
+++ b/openair2/PHY_INTERFACE/.gitignore
@@ -0,0 +1 @@
+/queue_test
diff --git a/openair2/PHY_INTERFACE/queue.c b/openair2/PHY_INTERFACE/queue.c
index 8f0173939e672f2aa41c32a0376715ab88c45938..1ae902f18d8843196aa0f05d4f16c9c578d72e0f 100644
--- a/openair2/PHY_INTERFACE/queue.c
+++ b/openair2/PHY_INTERFACE/queue.c
@@ -1,66 +1,153 @@
 #include "queue.h"
-#include "common/utils/LOG/log.h"
 #include <string.h>
+#include <assert.h>
+
+#ifdef UNITTEST
+#include <stdio.h>
+#define LOG_ERROR(MSG) printf(MSG "\n")
+#else
+#include "common/utils/LOG/log.h"
+#define LOG_ERROR(MSG) LOG_E(PHY, MSG "\n")
+#endif
 
-void init_queue(queue_t *q) {
-  memset(q, 0, sizeof(*q));
-  pthread_mutex_init(&q->mutex, NULL);
+void init_queue(queue_t *q)
+{
+    memset(q, 0, sizeof(*q));
+    pthread_mutex_init(&q->mutex, NULL);
 }
 
-bool put_queue(queue_t *q, void *item) {
-
-  if (pthread_mutex_lock(&q->mutex) != 0) {
-    LOG_E(PHY, "put_queue mutex_lock failed\n");
-    return false;
-  }
-
-  bool queued;
-  if (q->num_items >= MAX_QUEUE_SIZE) {
-    LOG_E(PHY, "Queue is full in put_queue\n");
-    queued = false;
-  } else {
-    q->items[q->write_index] = item;
-    q->write_index = (q->write_index + 1) % MAX_QUEUE_SIZE;
-    q->num_items++;
-    queued = true;
-  }
-
-  pthread_mutex_unlock(&q->mutex);
-  return queued;
+bool put_queue(queue_t *q, void *item)
+{
+    assert(item != NULL);
+    if (pthread_mutex_lock(&q->mutex) != 0)
+    {
+        LOG_ERROR("put_queue: mutex_lock failed");
+        return false;
+    }
+
+    bool queued;
+    if (q->num_items >= MAX_QUEUE_SIZE)
+    {
+        LOG_ERROR("put_queue: queue is full");
+        queued = false;
+    }
+    else
+    {
+        assert(q->items[q->write_index] == NULL);
+        q->items[q->write_index] = item;
+        q->write_index = (q->write_index + 1) % MAX_QUEUE_SIZE;
+        q->num_items++;
+        queued = true;
+    }
+
+    pthread_mutex_unlock(&q->mutex);
+    return queued;
 }
 
-void *get_queue(queue_t *q) {
+void *get_queue(queue_t *q)
+{
+    void *item = NULL;
+    if (pthread_mutex_lock(&q->mutex) != 0)
+    {
+        LOG_ERROR("get_queue: mutex_lock failed");
+        return NULL;
+    }
 
-  void *item = NULL;
-  if (pthread_mutex_lock(&q->mutex) != 0) {
-    LOG_E(PHY, "get_queue mutex_lock failed\n");
-    return NULL;
-  }
+    if (q->num_items > 0)
+    {
+        item = q->items[q->read_index];
+        assert(item != NULL);
+        q->items[q->read_index] = NULL;
+        q->read_index = (q->read_index + 1) % MAX_QUEUE_SIZE;
+        q->num_items--;
+    }
 
-  if (q->num_items > 0) {
-    item = q->items[q->read_index];
-    q->read_index = (q->read_index + 1) % MAX_QUEUE_SIZE;
-    q->num_items--;
-  }
+    pthread_mutex_unlock(&q->mutex);
+    return item;
+}
 
-  pthread_mutex_unlock(&q->mutex);
-  return item;
+bool requeue(queue_t *q, void *item)
+{
+    assert(item != NULL);
+    if (pthread_mutex_lock(&q->mutex) != 0)
+    {
+        LOG_ERROR("requeue: mutex_lock failed");
+        return false;
+    }
+
+    bool queued;
+    if (q->num_items >= MAX_QUEUE_SIZE)
+    {
+        LOG_ERROR("requeue: queue is full");
+        queued = false;
+    }
+    else
+    {
+        q->read_index = (q->read_index + MAX_QUEUE_SIZE - 1) % MAX_QUEUE_SIZE;
+        assert(q->items[q->read_index] == NULL);
+        q->items[q->read_index] = item;
+        q->num_items++;
+        queued = true;
+    }
+
+    pthread_mutex_unlock(&q->mutex);
+    return queued;
 }
 
 void *unqueue(queue_t *q)
 {
-  void *item = NULL;
-  if (pthread_mutex_lock(&q->mutex) != 0) {
-    LOG_E(PHY, "remove_from_back_of_queue mutex_lock failed\n");
-    return NULL;
-  }
-
-  if (q->num_items > 0) {
-    q->write_index = (q->write_index + MAX_QUEUE_SIZE - 1) % MAX_QUEUE_SIZE;
-    item = q->items[q->write_index];
-    q->num_items--;
-  }
-
-  pthread_mutex_unlock(&q->mutex);
-  return item;
+    void *item = NULL;
+    if (pthread_mutex_lock(&q->mutex) != 0) {
+        LOG_ERROR("unqueue: mutex_lock failed");
+        return NULL;
+    }
+
+    if (q->num_items > 0) {
+        q->write_index = (q->write_index + MAX_QUEUE_SIZE - 1) % MAX_QUEUE_SIZE;
+        item = q->items[q->write_index];
+        q->items[q->write_index] = NULL;
+        q->num_items--;
+    }
+
+    pthread_mutex_unlock(&q->mutex);
+    return item;
+}
+
+void *unqueue_matching(queue_t *q, queue_matcher_t *matcher, void *wanted)
+{
+    if (pthread_mutex_lock(&q->mutex) != 0)
+    {
+        LOG_ERROR("unqueue_matching: mutex_lock failed");
+        return NULL;
+    }
+
+    void *item = NULL;
+    size_t endi = q->write_index;
+    for (size_t i = 0; i < q->num_items; i++)
+    {
+        endi = (endi + MAX_QUEUE_SIZE - 1) % MAX_QUEUE_SIZE;
+        void *candidate = q->items[endi];
+        if (matcher(wanted, candidate))
+        {
+            item = candidate;
+            // delete item from the queue and move other items down
+            for (;;)
+            {
+                size_t j = (endi + 1) % MAX_QUEUE_SIZE;
+                if (j == q->write_index)
+                {
+                    q->items[endi] = NULL;
+                    q->write_index = endi;
+                    q->num_items--;
+                    break;
+                }
+                q->items[endi] = q->items[j];
+                endi = j;
+            }
+            break;
+        }
+    }
+
+    pthread_mutex_unlock(&q->mutex);
+    return item;
 }
diff --git a/openair2/PHY_INTERFACE/queue.h b/openair2/PHY_INTERFACE/queue.h
index a8fce7c4c258ba4db690ffbcb31423559ad9445e..15ad852abe68440f1496b413b24c57f3b200f16e 100644
--- a/openair2/PHY_INTERFACE/queue.h
+++ b/openair2/PHY_INTERFACE/queue.h
@@ -32,15 +32,30 @@
 
 #define MAX_QUEUE_SIZE 512
 
-typedef struct queue_t {
-  void *items[MAX_QUEUE_SIZE];
-  size_t read_index, write_index;
-  size_t num_items;
-  pthread_mutex_t mutex;
+typedef struct queue_t
+{
+    void *items[MAX_QUEUE_SIZE];
+    size_t read_index, write_index;
+    size_t num_items;
+    pthread_mutex_t mutex;
 } queue_t;
 
-
 void init_queue(queue_t *q);
 bool put_queue(queue_t *q, void *item);
 void *get_queue(queue_t *q);
+
+/* Put the given item back onto this queue at the head.
+   (The next call to put_queue would return this item.)
+   Return true if successful, false if the queue was full */
+bool requeue(queue_t *q, void *item);
+
+/* Remove the last item queued.
+   Return the item or NULL if the queue was empty */
 void *unqueue(queue_t *q);
+
+typedef bool queue_matcher_t(void *wanted, void *candidate);
+
+/* Unqueue the most recently queued item for watch `matcher(wanted, candidate)`
+   returns true where `candidate` is an item currently on the queue.
+   Returns the candidate item, or NULL if none matches */
+void *unqueue_matching(queue_t *q, queue_matcher_t *matcher, void *wanted);
diff --git a/openair2/PHY_INTERFACE/queue_test.c b/openair2/PHY_INTERFACE/queue_test.c
new file mode 100644
index 0000000000000000000000000000000000000000..020814cb9cf94d7b00c521d64ddb868a0841cc1a
--- /dev/null
+++ b/openair2/PHY_INTERFACE/queue_test.c
@@ -0,0 +1,201 @@
+#include "queue.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+#define FAIL do {                                                       \
+    printf("\n*** FAILED at %s line %d\n", __FILE__, __LINE__);         \
+    pass = false;                                                       \
+} while (0)
+
+#define EQUAL(A, B) do {                                                \
+    if ((A) != (B))                                                     \
+        FAIL;                                                           \
+} while (0)
+
+typedef uint32_t Thing_t;       /* actual type doesn't matter */
+
+static Thing_t things[MAX_QUEUE_SIZE];
+static Thing_t thing1, thing2;
+
+static bool matcher(void *wanted, void *candidate)
+{
+    return wanted == candidate;
+}
+
+int main(void)
+{
+    bool pass = true;
+    queue_t queue;
+    init_queue(&queue);
+
+    for (int i = 0; i < MAX_QUEUE_SIZE; ++i)
+    {
+        if (!put_queue(&queue, &things[i]))
+        {
+            FAIL;
+        }
+    }
+
+    /* queue is full */
+    if (put_queue(&queue, &thing1))
+        FAIL;
+
+    Thing_t *p;
+    for (int i = 0; i < MAX_QUEUE_SIZE; ++i)
+    {
+        p = get_queue(&queue);
+        EQUAL(p, &things[i]);
+    }
+
+    /* queue is empty */
+    p = get_queue(&queue);
+    EQUAL(p, NULL);
+
+    for (int i = 0; i < MAX_QUEUE_SIZE; ++i)
+    {
+        if (!put_queue(&queue, &things[i]))
+        {
+            FAIL;
+        }
+    }
+
+    p = get_queue(&queue);
+    EQUAL(p, &things[0]);
+
+    p = get_queue(&queue);
+    EQUAL(p, &things[1]);
+
+    if (!requeue(&queue, &thing1))
+        FAIL;
+    if (!requeue(&queue, &thing2))
+        FAIL;
+    p = get_queue(&queue);
+    EQUAL(p, &thing2);
+    p = get_queue(&queue);
+    EQUAL(p, &thing1);
+
+    if (!requeue(&queue, &things[1]))
+        FAIL;
+    if (!requeue(&queue, &things[0]))
+        FAIL;
+
+    for (int i = 0; i < MAX_QUEUE_SIZE / 2; ++i)
+    {
+        p = get_queue(&queue);
+        EQUAL(p, &things[i]);
+    }
+
+    for (int i = MAX_QUEUE_SIZE / 2; i < MAX_QUEUE_SIZE; ++i)
+    {
+        if (!put_queue(&queue, &things[i]))
+            FAIL;
+    }
+
+    p = get_queue(&queue);
+    EQUAL(p, &things[MAX_QUEUE_SIZE / 2]);
+    p = get_queue(&queue);
+    EQUAL(p, &things[MAX_QUEUE_SIZE / 2 + 1]);
+
+    // ---- unqueue ----
+
+    init_queue(&queue);
+    for (int i = 0; i < MAX_QUEUE_SIZE; ++i)
+    {
+        if (!put_queue(&queue, &things[i]))
+        {
+            FAIL;
+        }
+    }
+    for (int i = MAX_QUEUE_SIZE; --i >= 0;)
+    {
+        p = unqueue(&queue);
+        EQUAL(p, &things[i]);
+        EQUAL(queue.num_items, i);
+    }
+    EQUAL(queue.num_items, 0);
+    if (!put_queue(&queue, &thing1))
+        FAIL;
+    if (!put_queue(&queue, &thing2))
+        FAIL;
+    EQUAL(queue.num_items, 2);
+    p = get_queue(&queue);
+    EQUAL(p, &thing1);
+    p = get_queue(&queue);
+    EQUAL(p, &thing2);
+
+    // ---- unqueue_matching ----
+
+    init_queue(&queue);
+
+    // empty queue
+    p = unqueue_matching(&queue, matcher, &thing1);
+    EQUAL(p, NULL);
+    EQUAL(queue.num_items, 0);
+
+    // one item in queue
+    if (!put_queue(&queue, &thing1))
+        FAIL;
+    EQUAL(queue.num_items, 1);
+    p = unqueue_matching(&queue, matcher, &thing2);
+    EQUAL(p, NULL);
+    EQUAL(queue.num_items, 1);
+    p = unqueue_matching(&queue, matcher, &thing1);
+    EQUAL(p, &thing1);
+    EQUAL(queue.num_items, 0);
+
+    // fill the queue then remove every other item
+    for (int i = 0; i < MAX_QUEUE_SIZE; ++i)
+    {
+        if (!put_queue(&queue, &things[i]))
+        {
+            FAIL;
+        }
+    }
+    p = unqueue_matching(&queue, matcher, &thing1);
+    EQUAL(p, NULL);
+    for (int i = MAX_QUEUE_SIZE - 1; i >= 0; i -= 2)
+    {
+        p = unqueue_matching(&queue, matcher, &things[i]);
+        EQUAL(p, &things[i]);
+    }
+    EQUAL(queue.num_items, MAX_QUEUE_SIZE / 2);
+    p = unqueue_matching(&queue, matcher, &thing1);
+    EQUAL(p, NULL);
+    for (int i = 0; i < MAX_QUEUE_SIZE; i += 2)
+    {
+        p = get_queue(&queue);
+        EQUAL(p, &things[i]);
+    }
+    EQUAL(queue.num_items, 0);
+
+    // fill the queue then remove every third item
+    for (int i = 0; i < MAX_QUEUE_SIZE; ++i)
+    {
+        if (!put_queue(&queue, &things[i]))
+        {
+            FAIL;
+        }
+    }
+    p = unqueue_matching(&queue, matcher, &thing1);
+    EQUAL(p, NULL);
+    for (int i = 0; i < MAX_QUEUE_SIZE; i += 3)
+    {
+        p = unqueue_matching(&queue, matcher, &things[i]);
+        EQUAL(p, &things[i]);
+    }
+    EQUAL(queue.num_items, MAX_QUEUE_SIZE * 2 / 3);
+    p = unqueue_matching(&queue, matcher, &thing1);
+    EQUAL(p, NULL);
+    for (int i = 0; i < MAX_QUEUE_SIZE; ++i)
+    {
+        if (i % 3 == 0)
+            continue;
+        p = get_queue(&queue);
+        EQUAL(p, &things[i]);
+    }
+    EQUAL(queue.num_items, 0);
+
+    if (!pass)
+        return EXIT_FAILURE;
+    return EXIT_SUCCESS;
+}
diff --git a/openair2/PHY_INTERFACE/queue_test_run b/openair2/PHY_INTERFACE/queue_test_run
new file mode 100755
index 0000000000000000000000000000000000000000..afb3842eb334e4a99b18c109ee80114c706ba67d
--- /dev/null
+++ b/openair2/PHY_INTERFACE/queue_test_run
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+opts=(
+    -Wall -Werror
+    -Wno-error=int-to-pointer-cast
+    -Wno-int-to-pointer-cast
+    -DUNITTEST
+)
+
+set -x
+
+gcc "${opts[@]}" -fsanitize=address -o queue_test queue_test.c queue.c || exit
+./queue_test || exit
+
+gcc "${opts[@]}" -o queue_test queue_test.c queue.c || exit
+valgrind ./queue_test || exit
+
+: PASS