All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/3] block/accounting: fix unlocked latency histogram reconfiguration races
@ 2026-07-23 12:37 Denis V. Lunev
  2026-07-23 12:37 ` [PATCH 1/3] block/accounting: take stats->lock in latency histogram setters Denis V. Lunev
                   ` (2 more replies)
  0 siblings, 3 replies; 6+ messages in thread
From: Denis V. Lunev @ 2026-07-23 12:37 UTC (permalink / raw)
  To: qemu-block, qemu-devel
  Cc: den, Kevin Wolf, Hanna Reitz, Vladimir Sementsov-Ogievskiy

block_latency_histogram_set() and block_latency_histograms_clear()
replace BlockLatencyHistogram's nbins/boundaries/bins without taking
stats->lock, while block_account_one_io() reads those same fields
under that lock from whatever iothread completes the I/O. A histogram
reconfiguration (block-latency-histogram-set QMP command, monitor
thread) racing an in-flight completion can therefore observe those
fields torn, hitting assert(pos != NULL) in
block_latency_histogram_account(), or corrupting the heap outright.
This showed up as a qemu-kvm SIGABRT on a customer's virtio-blk guest.

Regression test is added for illustrative purpose but I am unsure that
it is viable long term. Feel free to drop.

Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Kevin Wolf <kwolf@redhat.com>
CC: Hanna Reitz <hreitz@redhat.com>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>

Denis V. Lunev (3):
  block/accounting: take stats->lock in latency histogram setters
  block/qapi: take stats->lock when reading BlockAcctStats for
    query-blockstats
  tests/unit: add reproducer for BlockAcctStats histogram locking race

 block/accounting.c                 |  34 +++++----
 block/qapi.c                       | 102 +++++++++++++++-----------
 tests/unit/meson.build             |   1 +
 tests/unit/test-block-accounting.c | 114 +++++++++++++++++++++++++++++
 4 files changed, 194 insertions(+), 57 deletions(-)
 create mode 100644 tests/unit/test-block-accounting.c

-- 
2.53.0



^ permalink raw reply	[flat|nested] 6+ messages in thread

* [PATCH 1/3] block/accounting: take stats->lock in latency histogram setters
  2026-07-23 12:37 [PATCH 0/3] block/accounting: fix unlocked latency histogram reconfiguration races Denis V. Lunev
@ 2026-07-23 12:37 ` Denis V. Lunev
  2026-07-23 12:37 ` [PATCH 2/3] block/qapi: take stats->lock when reading BlockAcctStats for query-blockstats Denis V. Lunev
  2026-07-23 12:37 ` [PATCH 3/3] tests/unit: add reproducer for BlockAcctStats histogram locking race Denis V. Lunev
  2 siblings, 0 replies; 6+ messages in thread
From: Denis V. Lunev @ 2026-07-23 12:37 UTC (permalink / raw)
  To: qemu-block, qemu-devel
  Cc: den, Kevin Wolf, Hanna Reitz, Vladimir Sementsov-Ogievskiy

block_latency_histogram_set() and block_latency_histograms_clear()
replace BlockLatencyHistogram's nbins/boundaries/bins without taking
stats->lock, while block_account_one_io() reads those same fields
under that lock from whatever iothread completes the I/O. The result
is usual use-after-free and qemu crash.

Take stats->lock in both setters, matching the lock already held by
the reader.

Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Kevin Wolf <kwolf@redhat.com>
CC: Hanna Reitz <hreitz@redhat.com>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
 block/accounting.c | 34 +++++++++++++++++++---------------
 1 file changed, 19 insertions(+), 15 deletions(-)

diff --git a/block/accounting.c b/block/accounting.c
index f00fe99740..c277b796eb 100644
--- a/block/accounting.c
+++ b/block/accounting.c
@@ -194,17 +194,19 @@ int block_latency_histogram_set(BlockAcctStats *stats, enum BlockAcctType type,
         return -EINVAL;
     }
 
-    hist->nbins = new_nbins;
-    g_free(hist->boundaries);
-    hist->boundaries = g_new(uint64_t, hist->nbins - 1);
-    for (entry = boundaries, ptr = hist->boundaries; entry;
-         entry = entry->next, ptr++)
-    {
-        *ptr = entry->value;
-    }
+    WITH_QEMU_LOCK_GUARD(&stats->lock) {
+        hist->nbins = new_nbins;
+        g_free(hist->boundaries);
+        hist->boundaries = g_new(uint64_t, hist->nbins - 1);
+        for (entry = boundaries, ptr = hist->boundaries; entry;
+             entry = entry->next, ptr++)
+        {
+            *ptr = entry->value;
+        }
 
-    g_free(hist->bins);
-    hist->bins = g_new0(uint64_t, hist->nbins);
+        g_free(hist->bins);
+        hist->bins = g_new0(uint64_t, hist->nbins);
+    }
 
     return 0;
 }
@@ -213,11 +215,13 @@ void block_latency_histograms_clear(BlockAcctStats *stats)
 {
     int i;
 
-    for (i = 0; i < BLOCK_MAX_IOTYPE; i++) {
-        BlockLatencyHistogram *hist = &stats->latency_histogram[i];
-        g_free(hist->bins);
-        g_free(hist->boundaries);
-        memset(hist, 0, sizeof(*hist));
+    WITH_QEMU_LOCK_GUARD(&stats->lock) {
+        for (i = 0; i < BLOCK_MAX_IOTYPE; i++) {
+            BlockLatencyHistogram *hist = &stats->latency_histogram[i];
+            g_free(hist->bins);
+            g_free(hist->boundaries);
+            memset(hist, 0, sizeof(*hist));
+        }
     }
 }
 
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 2/3] block/qapi: take stats->lock when reading BlockAcctStats for query-blockstats
  2026-07-23 12:37 [PATCH 0/3] block/accounting: fix unlocked latency histogram reconfiguration races Denis V. Lunev
  2026-07-23 12:37 ` [PATCH 1/3] block/accounting: take stats->lock in latency histogram setters Denis V. Lunev
@ 2026-07-23 12:37 ` Denis V. Lunev
  2026-07-23 14:02   ` Andrey Drobyshev
  2026-07-23 12:37 ` [PATCH 3/3] tests/unit: add reproducer for BlockAcctStats histogram locking race Denis V. Lunev
  2 siblings, 1 reply; 6+ messages in thread
From: Denis V. Lunev @ 2026-07-23 12:37 UTC (permalink / raw)
  To: qemu-block, qemu-devel
  Cc: den, Kevin Wolf, Hanna Reitz, Vladimir Sementsov-Ogievskiy

bdrv_query_blk_stats() reads nr_bytes[], nr_ops[], failed_ops[],
invalid_ops[], merged[], total_time_ns[], last_access_time_ns, and
the latency histogram bins directly off BlockAcctStats, without
taking stats->lock. block_account_one_io() updates the same fields
under that lock from whatever iothread completes the I/O, so this is
an unsynchronized read of concurrently-updated state.

This is a narrower issue than the one fixed for the histogram
setters. query-blockstats and block-latency-histogram-set are both
plain (non-allow-oob) QMP commands, so they always run serialized on
the monitor thread and can never execute concurrently with each
other.

Take stats->lock around the read for consistency.

Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Kevin Wolf <kwolf@redhat.com>
CC: Hanna Reitz <hreitz@redhat.com>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
 block/qapi.c | 102 ++++++++++++++++++++++++++++++---------------------
 1 file changed, 60 insertions(+), 42 deletions(-)

diff --git a/block/qapi.c b/block/qapi.c
index eabfbfc258..c3f20832b7 100644
--- a/block/qapi.c
+++ b/block/qapi.c
@@ -535,49 +535,67 @@ static void bdrv_query_blk_stats(BlockDeviceStats *ds, BlockBackend *blk)
     BlockAcctTimedStats *ts = NULL;
     BlockLatencyHistogram *hgram;
 
-    ds->rd_bytes = stats->nr_bytes[BLOCK_ACCT_READ];
-    ds->wr_bytes = stats->nr_bytes[BLOCK_ACCT_WRITE];
-    ds->zone_append_bytes = stats->nr_bytes[BLOCK_ACCT_ZONE_APPEND];
-    ds->unmap_bytes = stats->nr_bytes[BLOCK_ACCT_UNMAP];
-    ds->rd_operations = stats->nr_ops[BLOCK_ACCT_READ];
-    ds->wr_operations = stats->nr_ops[BLOCK_ACCT_WRITE];
-    ds->zone_append_operations = stats->nr_ops[BLOCK_ACCT_ZONE_APPEND];
-    ds->unmap_operations = stats->nr_ops[BLOCK_ACCT_UNMAP];
-
-    ds->failed_rd_operations = stats->failed_ops[BLOCK_ACCT_READ];
-    ds->failed_wr_operations = stats->failed_ops[BLOCK_ACCT_WRITE];
-    ds->failed_zone_append_operations =
-        stats->failed_ops[BLOCK_ACCT_ZONE_APPEND];
-    ds->failed_flush_operations = stats->failed_ops[BLOCK_ACCT_FLUSH];
-    ds->failed_unmap_operations = stats->failed_ops[BLOCK_ACCT_UNMAP];
-
-    ds->invalid_rd_operations = stats->invalid_ops[BLOCK_ACCT_READ];
-    ds->invalid_wr_operations = stats->invalid_ops[BLOCK_ACCT_WRITE];
-    ds->invalid_zone_append_operations =
-        stats->invalid_ops[BLOCK_ACCT_ZONE_APPEND];
-    ds->invalid_flush_operations =
-        stats->invalid_ops[BLOCK_ACCT_FLUSH];
-    ds->invalid_unmap_operations = stats->invalid_ops[BLOCK_ACCT_UNMAP];
-
-    ds->rd_merged = stats->merged[BLOCK_ACCT_READ];
-    ds->wr_merged = stats->merged[BLOCK_ACCT_WRITE];
-    ds->zone_append_merged = stats->merged[BLOCK_ACCT_ZONE_APPEND];
-    ds->unmap_merged = stats->merged[BLOCK_ACCT_UNMAP];
-    ds->flush_operations = stats->nr_ops[BLOCK_ACCT_FLUSH];
-    ds->wr_total_time_ns = stats->total_time_ns[BLOCK_ACCT_WRITE];
-    ds->zone_append_total_time_ns =
-        stats->total_time_ns[BLOCK_ACCT_ZONE_APPEND];
-    ds->rd_total_time_ns = stats->total_time_ns[BLOCK_ACCT_READ];
-    ds->flush_total_time_ns = stats->total_time_ns[BLOCK_ACCT_FLUSH];
-    ds->unmap_total_time_ns = stats->total_time_ns[BLOCK_ACCT_UNMAP];
-
-    ds->has_idle_time_ns = stats->last_access_time_ns > 0;
-    if (ds->has_idle_time_ns) {
-        ds->idle_time_ns = block_acct_idle_time_ns(stats);
-    }
+    /*
+     * nr_bytes[] etc. are mutated under stats->lock by block_account_one_io()
+     * from an iothread, and latency_histogram[].{nbins,boundaries,bins} can
+     * be freed and reallocated by a concurrent block_latency_histogram_set()
+     * on the monitor thread, so every read here needs the same lock.
+     */
+    WITH_QEMU_LOCK_GUARD(&stats->lock) {
+        ds->rd_bytes = stats->nr_bytes[BLOCK_ACCT_READ];
+        ds->wr_bytes = stats->nr_bytes[BLOCK_ACCT_WRITE];
+        ds->zone_append_bytes = stats->nr_bytes[BLOCK_ACCT_ZONE_APPEND];
+        ds->unmap_bytes = stats->nr_bytes[BLOCK_ACCT_UNMAP];
+        ds->rd_operations = stats->nr_ops[BLOCK_ACCT_READ];
+        ds->wr_operations = stats->nr_ops[BLOCK_ACCT_WRITE];
+        ds->zone_append_operations = stats->nr_ops[BLOCK_ACCT_ZONE_APPEND];
+        ds->unmap_operations = stats->nr_ops[BLOCK_ACCT_UNMAP];
+
+        ds->failed_rd_operations = stats->failed_ops[BLOCK_ACCT_READ];
+        ds->failed_wr_operations = stats->failed_ops[BLOCK_ACCT_WRITE];
+        ds->failed_zone_append_operations =
+            stats->failed_ops[BLOCK_ACCT_ZONE_APPEND];
+        ds->failed_flush_operations = stats->failed_ops[BLOCK_ACCT_FLUSH];
+        ds->failed_unmap_operations = stats->failed_ops[BLOCK_ACCT_UNMAP];
+
+        ds->invalid_rd_operations = stats->invalid_ops[BLOCK_ACCT_READ];
+        ds->invalid_wr_operations = stats->invalid_ops[BLOCK_ACCT_WRITE];
+        ds->invalid_zone_append_operations =
+            stats->invalid_ops[BLOCK_ACCT_ZONE_APPEND];
+        ds->invalid_flush_operations =
+            stats->invalid_ops[BLOCK_ACCT_FLUSH];
+        ds->invalid_unmap_operations = stats->invalid_ops[BLOCK_ACCT_UNMAP];
+
+        ds->rd_merged = stats->merged[BLOCK_ACCT_READ];
+        ds->wr_merged = stats->merged[BLOCK_ACCT_WRITE];
+        ds->zone_append_merged = stats->merged[BLOCK_ACCT_ZONE_APPEND];
+        ds->unmap_merged = stats->merged[BLOCK_ACCT_UNMAP];
+        ds->flush_operations = stats->nr_ops[BLOCK_ACCT_FLUSH];
+        ds->wr_total_time_ns = stats->total_time_ns[BLOCK_ACCT_WRITE];
+        ds->zone_append_total_time_ns =
+            stats->total_time_ns[BLOCK_ACCT_ZONE_APPEND];
+        ds->rd_total_time_ns = stats->total_time_ns[BLOCK_ACCT_READ];
+        ds->flush_total_time_ns = stats->total_time_ns[BLOCK_ACCT_FLUSH];
+        ds->unmap_total_time_ns = stats->total_time_ns[BLOCK_ACCT_UNMAP];
+
+        ds->has_idle_time_ns = stats->last_access_time_ns > 0;
+        if (ds->has_idle_time_ns) {
+            ds->idle_time_ns = block_acct_idle_time_ns(stats);
+        }
 
-    ds->account_invalid = stats->account_invalid;
-    ds->account_failed = stats->account_failed;
+        ds->account_invalid = stats->account_invalid;
+        ds->account_failed = stats->account_failed;
+
+        hgram = stats->latency_histogram;
+        ds->rd_latency_histogram
+            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_READ]);
+        ds->wr_latency_histogram
+            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_WRITE]);
+        ds->zone_append_latency_histogram
+            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_ZONE_APPEND]);
+        ds->flush_latency_histogram
+            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_FLUSH]);
+    }
 
     while ((ts = block_acct_interval_next(stats, ts))) {
         BlockDeviceTimedStats *dev_stats = g_malloc0(sizeof(*dev_stats));
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* [PATCH 3/3] tests/unit: add reproducer for BlockAcctStats histogram locking race
  2026-07-23 12:37 [PATCH 0/3] block/accounting: fix unlocked latency histogram reconfiguration races Denis V. Lunev
  2026-07-23 12:37 ` [PATCH 1/3] block/accounting: take stats->lock in latency histogram setters Denis V. Lunev
  2026-07-23 12:37 ` [PATCH 2/3] block/qapi: take stats->lock when reading BlockAcctStats for query-blockstats Denis V. Lunev
@ 2026-07-23 12:37 ` Denis V. Lunev
  2026-07-23 14:04   ` Andrey Drobyshev
  2 siblings, 1 reply; 6+ messages in thread
From: Denis V. Lunev @ 2026-07-23 12:37 UTC (permalink / raw)
  To: qemu-block, qemu-devel
  Cc: den, Kevin Wolf, Hanna Reitz, Vladimir Sementsov-Ogievskiy

block_latency_histogram_set() and block_latency_histograms_clear()
replace BlockLatencyHistogram's nbins/boundaries/bins without taking
stats->lock, while block_account_one_io() reads those same fields
under that lock from whatever iothread completes the I/O.

Add a test that races two real threads against
block_latency_histogram_set() and
block_acct_start()/block_acct_done() on the same BlockAcctStats.
Applied here it passes, since the previous two commits already take
the lock; reverting them locally reproduces the abort this series
fixes, in about a second.

Signed-off-by: Denis V. Lunev <den@openvz.org>
CC: Kevin Wolf <kwolf@redhat.com>
CC: Hanna Reitz <hreitz@redhat.com>
CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
---
 tests/unit/meson.build             |   1 +
 tests/unit/test-block-accounting.c | 114 +++++++++++++++++++++++++++++
 2 files changed, 115 insertions(+)
 create mode 100644 tests/unit/test-block-accounting.c

diff --git a/tests/unit/meson.build b/tests/unit/meson.build
index 5ba6b1a230..dc3fb954c0 100644
--- a/tests/unit/meson.build
+++ b/tests/unit/meson.build
@@ -75,6 +75,7 @@ if have_block
     'test-blockjob': [testblock],
     'test-blockjob-txn': [testblock],
     'test-block-backend': [testblock],
+    'test-block-accounting': [testblock],
     'test-block-iothread': [testblock],
     'test-write-threshold': [testblock],
     'test-crypto-hash': [crypto],
diff --git a/tests/unit/test-block-accounting.c b/tests/unit/test-block-accounting.c
new file mode 100644
index 0000000000..e749b73749
--- /dev/null
+++ b/tests/unit/test-block-accounting.c
@@ -0,0 +1,114 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * BlockAcctStats latency histogram locking regression test
+ *
+ * Copyright (c) 2026 Virtuozzo International GmbH.
+ *
+ * Regression test for missing stats->lock in
+ * block_latency_histogram_set()/block_latency_histograms_clear(),
+ * racing block_account_one_io() reading the same fields from an
+ * iothread. Aborts reliably before the fix, passes after it.
+ */
+
+#include "qemu/osdep.h"
+#include "block/block.h"
+#include "block/accounting.h"
+#include "system/block-backend.h"
+#include "system/block-backend-io.h"
+#include "qapi/error.h"
+#include "qemu/main-loop.h"
+#include "qemu/thread.h"
+
+#define RACE_DURATION_MS 2000
+#define NUM_READER_THREADS 8
+
+static bool stop_workers;
+
+/*
+ * Different bin counts, so the writer's g_free()/g_new() churn can be
+ * caught mid-update. Values are small enough (nanoseconds) that plain
+ * back-to-back start/done calls exercise every bin without sleeping.
+ */
+static uint64List boundaries_a[] = {
+    { .next = &boundaries_a[1], .value = 1000 },
+    { .next = &boundaries_a[2], .value = 5000 },
+    { .next = NULL,             .value = 50000 },
+};
+
+static uint64List boundaries_b[] = {
+    { .next = &boundaries_b[1], .value = 800 },
+    { .next = &boundaries_b[2], .value = 3000 },
+    { .next = &boundaries_b[3], .value = 20000 },
+    { .next = NULL,             .value = 200000 },
+};
+
+static void *writer_thread(void *opaque)
+{
+    BlockAcctStats *stats = opaque;
+
+    while (!qatomic_read(&stop_workers)) {
+        block_latency_histogram_set(stats, BLOCK_ACCT_READ, boundaries_a);
+        block_latency_histogram_set(stats, BLOCK_ACCT_READ, boundaries_b);
+    }
+
+    return NULL;
+}
+
+static void *reader_thread(void *opaque)
+{
+    BlockAcctStats *stats = opaque;
+
+    while (!qatomic_read(&stop_workers)) {
+        BlockAcctCookie cookie;
+
+        block_acct_start(stats, &cookie, 4096, BLOCK_ACCT_READ);
+        block_acct_done(stats, &cookie);
+    }
+
+    return NULL;
+}
+
+static void test_latency_histogram_race(void)
+{
+    BlockBackend *blk = blk_new(qemu_get_aio_context(),
+                                BLK_PERM_ALL, BLK_PERM_ALL);
+    BlockAcctStats *stats = blk_get_stats(blk);
+    QemuThread writer, readers[NUM_READER_THREADS];
+    int i;
+
+    /* Histogram has to be enabled (bins != NULL) before racing it. */
+    g_assert(block_latency_histogram_set(stats, BLOCK_ACCT_READ,
+                                         boundaries_a) == 0);
+
+    stop_workers = false;
+    qemu_thread_create(&writer, "hist-writer", writer_thread, stats,
+                       QEMU_THREAD_JOINABLE);
+    for (i = 0; i < NUM_READER_THREADS; i++) {
+        qemu_thread_create(&readers[i], "hist-reader", reader_thread, stats,
+                           QEMU_THREAD_JOINABLE);
+    }
+
+    g_usleep(RACE_DURATION_MS * 1000);
+    qatomic_set(&stop_workers, true);
+
+    qemu_thread_join(&writer);
+    for (i = 0; i < NUM_READER_THREADS; i++) {
+        qemu_thread_join(&readers[i]);
+    }
+
+    blk_unref(blk);
+}
+
+int main(int argc, char **argv)
+{
+    bdrv_init();
+    qemu_init_main_loop(&error_abort);
+
+    g_test_init(&argc, &argv, NULL);
+
+    g_test_add_func("/block-accounting/latency_histogram_race",
+                    test_latency_histogram_race);
+
+    return g_test_run();
+}
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [PATCH 2/3] block/qapi: take stats->lock when reading BlockAcctStats for query-blockstats
  2026-07-23 12:37 ` [PATCH 2/3] block/qapi: take stats->lock when reading BlockAcctStats for query-blockstats Denis V. Lunev
@ 2026-07-23 14:02   ` Andrey Drobyshev
  0 siblings, 0 replies; 6+ messages in thread
From: Andrey Drobyshev @ 2026-07-23 14:02 UTC (permalink / raw)
  To: Denis V. Lunev, qemu-block, qemu-devel
  Cc: Kevin Wolf, Hanna Reitz, Vladimir Sementsov-Ogievskiy

On 7/23/26 3:37 PM, Denis V. Lunev wrote:
> bdrv_query_blk_stats() reads nr_bytes[], nr_ops[], failed_ops[],
> invalid_ops[], merged[], total_time_ns[], last_access_time_ns, and
> the latency histogram bins directly off BlockAcctStats, without
> taking stats->lock. block_account_one_io() updates the same fields
> under that lock from whatever iothread completes the I/O, so this is
> an unsynchronized read of concurrently-updated state.
> 
> This is a narrower issue than the one fixed for the histogram
> setters. query-blockstats and block-latency-histogram-set are both
> plain (non-allow-oob) QMP commands, so they always run serialized on
> the monitor thread and can never execute concurrently with each
> other.
> 
> Take stats->lock around the read for consistency.
> 
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> CC: Kevin Wolf <kwolf@redhat.com>
> CC: Hanna Reitz <hreitz@redhat.com>
> CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
> ---
>  block/qapi.c | 102 ++++++++++++++++++++++++++++++---------------------
>  1 file changed, 60 insertions(+), 42 deletions(-)
> 
> diff --git a/block/qapi.c b/block/qapi.c
> index eabfbfc258..c3f20832b7 100644
> --- a/block/qapi.c
> +++ b/block/qapi.c
> @@ -535,49 +535,67 @@ static void bdrv_query_blk_stats(BlockDeviceStats *ds, BlockBackend *blk)
>      BlockAcctTimedStats *ts = NULL;
>      BlockLatencyHistogram *hgram;
>  
> -    ds->rd_bytes = stats->nr_bytes[BLOCK_ACCT_READ];
> -    ds->wr_bytes = stats->nr_bytes[BLOCK_ACCT_WRITE];
> -    ds->zone_append_bytes = stats->nr_bytes[BLOCK_ACCT_ZONE_APPEND];
> -    ds->unmap_bytes = stats->nr_bytes[BLOCK_ACCT_UNMAP];
> -    ds->rd_operations = stats->nr_ops[BLOCK_ACCT_READ];
> -    ds->wr_operations = stats->nr_ops[BLOCK_ACCT_WRITE];
> -    ds->zone_append_operations = stats->nr_ops[BLOCK_ACCT_ZONE_APPEND];
> -    ds->unmap_operations = stats->nr_ops[BLOCK_ACCT_UNMAP];
> -
> -    ds->failed_rd_operations = stats->failed_ops[BLOCK_ACCT_READ];
> -    ds->failed_wr_operations = stats->failed_ops[BLOCK_ACCT_WRITE];
> -    ds->failed_zone_append_operations =
> -        stats->failed_ops[BLOCK_ACCT_ZONE_APPEND];
> -    ds->failed_flush_operations = stats->failed_ops[BLOCK_ACCT_FLUSH];
> -    ds->failed_unmap_operations = stats->failed_ops[BLOCK_ACCT_UNMAP];
> -
> -    ds->invalid_rd_operations = stats->invalid_ops[BLOCK_ACCT_READ];
> -    ds->invalid_wr_operations = stats->invalid_ops[BLOCK_ACCT_WRITE];
> -    ds->invalid_zone_append_operations =
> -        stats->invalid_ops[BLOCK_ACCT_ZONE_APPEND];
> -    ds->invalid_flush_operations =
> -        stats->invalid_ops[BLOCK_ACCT_FLUSH];
> -    ds->invalid_unmap_operations = stats->invalid_ops[BLOCK_ACCT_UNMAP];
> -
> -    ds->rd_merged = stats->merged[BLOCK_ACCT_READ];
> -    ds->wr_merged = stats->merged[BLOCK_ACCT_WRITE];
> -    ds->zone_append_merged = stats->merged[BLOCK_ACCT_ZONE_APPEND];
> -    ds->unmap_merged = stats->merged[BLOCK_ACCT_UNMAP];
> -    ds->flush_operations = stats->nr_ops[BLOCK_ACCT_FLUSH];
> -    ds->wr_total_time_ns = stats->total_time_ns[BLOCK_ACCT_WRITE];
> -    ds->zone_append_total_time_ns =
> -        stats->total_time_ns[BLOCK_ACCT_ZONE_APPEND];
> -    ds->rd_total_time_ns = stats->total_time_ns[BLOCK_ACCT_READ];
> -    ds->flush_total_time_ns = stats->total_time_ns[BLOCK_ACCT_FLUSH];
> -    ds->unmap_total_time_ns = stats->total_time_ns[BLOCK_ACCT_UNMAP];
> -
> -    ds->has_idle_time_ns = stats->last_access_time_ns > 0;
> -    if (ds->has_idle_time_ns) {
> -        ds->idle_time_ns = block_acct_idle_time_ns(stats);
> -    }
> +    /*
> +     * nr_bytes[] etc. are mutated under stats->lock by block_account_one_io()
> +     * from an iothread, and latency_histogram[].{nbins,boundaries,bins} can
> +     * be freed and reallocated by a concurrent block_latency_histogram_set()
> +     * on the monitor thread, so every read here needs the same lock.
> +     */
> +    WITH_QEMU_LOCK_GUARD(&stats->lock) {
> +        ds->rd_bytes = stats->nr_bytes[BLOCK_ACCT_READ];
> +        ds->wr_bytes = stats->nr_bytes[BLOCK_ACCT_WRITE];
> +        ds->zone_append_bytes = stats->nr_bytes[BLOCK_ACCT_ZONE_APPEND];
> +        ds->unmap_bytes = stats->nr_bytes[BLOCK_ACCT_UNMAP];
> +        ds->rd_operations = stats->nr_ops[BLOCK_ACCT_READ];
> +        ds->wr_operations = stats->nr_ops[BLOCK_ACCT_WRITE];
> +        ds->zone_append_operations = stats->nr_ops[BLOCK_ACCT_ZONE_APPEND];
> +        ds->unmap_operations = stats->nr_ops[BLOCK_ACCT_UNMAP];
> +
> +        ds->failed_rd_operations = stats->failed_ops[BLOCK_ACCT_READ];
> +        ds->failed_wr_operations = stats->failed_ops[BLOCK_ACCT_WRITE];
> +        ds->failed_zone_append_operations =
> +            stats->failed_ops[BLOCK_ACCT_ZONE_APPEND];
> +        ds->failed_flush_operations = stats->failed_ops[BLOCK_ACCT_FLUSH];
> +        ds->failed_unmap_operations = stats->failed_ops[BLOCK_ACCT_UNMAP];
> +
> +        ds->invalid_rd_operations = stats->invalid_ops[BLOCK_ACCT_READ];
> +        ds->invalid_wr_operations = stats->invalid_ops[BLOCK_ACCT_WRITE];
> +        ds->invalid_zone_append_operations =
> +            stats->invalid_ops[BLOCK_ACCT_ZONE_APPEND];
> +        ds->invalid_flush_operations =
> +            stats->invalid_ops[BLOCK_ACCT_FLUSH];
> +        ds->invalid_unmap_operations = stats->invalid_ops[BLOCK_ACCT_UNMAP];
> +
> +        ds->rd_merged = stats->merged[BLOCK_ACCT_READ];
> +        ds->wr_merged = stats->merged[BLOCK_ACCT_WRITE];
> +        ds->zone_append_merged = stats->merged[BLOCK_ACCT_ZONE_APPEND];
> +        ds->unmap_merged = stats->merged[BLOCK_ACCT_UNMAP];
> +        ds->flush_operations = stats->nr_ops[BLOCK_ACCT_FLUSH];
> +        ds->wr_total_time_ns = stats->total_time_ns[BLOCK_ACCT_WRITE];
> +        ds->zone_append_total_time_ns =
> +            stats->total_time_ns[BLOCK_ACCT_ZONE_APPEND];
> +        ds->rd_total_time_ns = stats->total_time_ns[BLOCK_ACCT_READ];
> +        ds->flush_total_time_ns = stats->total_time_ns[BLOCK_ACCT_FLUSH];
> +        ds->unmap_total_time_ns = stats->total_time_ns[BLOCK_ACCT_UNMAP];
> +
> +        ds->has_idle_time_ns = stats->last_access_time_ns > 0;
> +        if (ds->has_idle_time_ns) {
> +            ds->idle_time_ns = block_acct_idle_time_ns(stats);
> +        }
>  
> -    ds->account_invalid = stats->account_invalid;
> -    ds->account_failed = stats->account_failed;
> +        ds->account_invalid = stats->account_invalid;
> +        ds->account_failed = stats->account_failed;
> +
> +        hgram = stats->latency_histogram;
> +        ds->rd_latency_histogram
> +            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_READ]);
> +        ds->wr_latency_histogram
> +            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_WRITE]);
> +        ds->zone_append_latency_histogram
> +            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_ZONE_APPEND]);
> +        ds->flush_latency_histogram
> +            = bdrv_latency_histogram_stats(&hgram[BLOCK_ACCT_FLUSH]);
> +    }

Smth is off with this hunk.  These calls to
bdrv_latency_histogram_stats() pre-exist below (after the while loop).
If they need to be protected by the lock as well, we should remove those
duplicating calls below.

>  
>      while ((ts = block_acct_interval_next(stats, ts))) {
>          BlockDeviceTimedStats *dev_stats = g_malloc0(sizeof(*dev_stats));

Also in the while loop we call timed_average_{min,max,avg} with no
locks, whereas block_account_one_io() does update the intervals under
the lock.  Shouldn't we move the while loop itself under the lock guard?

Andrey


^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [PATCH 3/3] tests/unit: add reproducer for BlockAcctStats histogram locking race
  2026-07-23 12:37 ` [PATCH 3/3] tests/unit: add reproducer for BlockAcctStats histogram locking race Denis V. Lunev
@ 2026-07-23 14:04   ` Andrey Drobyshev
  0 siblings, 0 replies; 6+ messages in thread
From: Andrey Drobyshev @ 2026-07-23 14:04 UTC (permalink / raw)
  To: Denis V. Lunev, qemu-block, qemu-devel
  Cc: Kevin Wolf, Hanna Reitz, Vladimir Sementsov-Ogievskiy

On 7/23/26 3:37 PM, Denis V. Lunev wrote:
> block_latency_histogram_set() and block_latency_histograms_clear()
> replace BlockLatencyHistogram's nbins/boundaries/bins without taking
> stats->lock, while block_account_one_io() reads those same fields
> under that lock from whatever iothread completes the I/O.
> 
> Add a test that races two real threads against
> block_latency_histogram_set() and
> block_acct_start()/block_acct_done() on the same BlockAcctStats.
> Applied here it passes, since the previous two commits already take
> the lock; reverting them locally reproduces the abort this series
> fixes, in about a second.
> 
> Signed-off-by: Denis V. Lunev <den@openvz.org>
> CC: Kevin Wolf <kwolf@redhat.com>
> CC: Hanna Reitz <hreitz@redhat.com>
> CC: Vladimir Sementsov-Ogievskiy <vsementsov@yandex-team.ru>
> ---
>  tests/unit/meson.build             |   1 +
>  tests/unit/test-block-accounting.c | 114 +++++++++++++++++++++++++++++
>  2 files changed, 115 insertions(+)
>  create mode 100644 tests/unit/test-block-accounting.c
> 
> diff --git a/tests/unit/meson.build b/tests/unit/meson.build
> index 5ba6b1a230..dc3fb954c0 100644
> --- a/tests/unit/meson.build
> +++ b/tests/unit/meson.build
> @@ -75,6 +75,7 @@ if have_block
>      'test-blockjob': [testblock],
>      'test-blockjob-txn': [testblock],
>      'test-block-backend': [testblock],
> +    'test-block-accounting': [testblock],
>      'test-block-iothread': [testblock],
>      'test-write-threshold': [testblock],
>      'test-crypto-hash': [crypto],
> diff --git a/tests/unit/test-block-accounting.c b/tests/unit/test-block-accounting.c
> new file mode 100644
> index 0000000000..e749b73749
> --- /dev/null
> +++ b/tests/unit/test-block-accounting.c
> @@ -0,0 +1,114 @@
> +/*
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + *
> + * BlockAcctStats latency histogram locking regression test
> + *
> + * Copyright (c) 2026 Virtuozzo International GmbH.
> + *
> + * Regression test for missing stats->lock in
> + * block_latency_histogram_set()/block_latency_histograms_clear(),
> + * racing block_account_one_io() reading the same fields from an
> + * iothread. Aborts reliably before the fix, passes after it.
> + */
> +
> +#include "qemu/osdep.h"
> +#include "block/block.h"
> +#include "block/accounting.h"
> +#include "system/block-backend.h"
> +#include "system/block-backend-io.h"
> +#include "qapi/error.h"
> +#include "qemu/main-loop.h"
> +#include "qemu/thread.h"
> +
> +#define RACE_DURATION_MS 2000
> +#define NUM_READER_THREADS 8
> +
> +static bool stop_workers;
> +
> +/*
> + * Different bin counts, so the writer's g_free()/g_new() churn can be
> + * caught mid-update. Values are small enough (nanoseconds) that plain
> + * back-to-back start/done calls exercise every bin without sleeping.
> + */
> +static uint64List boundaries_a[] = {
> +    { .next = &boundaries_a[1], .value = 1000 },
> +    { .next = &boundaries_a[2], .value = 5000 },
> +    { .next = NULL,             .value = 50000 },
> +};
> +
> +static uint64List boundaries_b[] = {
> +    { .next = &boundaries_b[1], .value = 800 },
> +    { .next = &boundaries_b[2], .value = 3000 },
> +    { .next = &boundaries_b[3], .value = 20000 },
> +    { .next = NULL,             .value = 200000 },
> +};
> +
> +static void *writer_thread(void *opaque)
> +{
> +    BlockAcctStats *stats = opaque;
> +
> +    while (!qatomic_read(&stop_workers)) {
> +        block_latency_histogram_set(stats, BLOCK_ACCT_READ, boundaries_a);
> +        block_latency_histogram_set(stats, BLOCK_ACCT_READ, boundaries_b);

We fixed block_latency_histograms_clear() in the 1st patch but don't
cover it here in the test.  Should we add a call to
block_latency_histograms_clear(stats) here in the end of while loop to
get it covered?

Andrey

> +    }
> +
> +    return NULL;
> +}
> +
> +static void *reader_thread(void *opaque)
> +{
> +    BlockAcctStats *stats = opaque;
> +
> +    while (!qatomic_read(&stop_workers)) {
> +        BlockAcctCookie cookie;
> +
> +        block_acct_start(stats, &cookie, 4096, BLOCK_ACCT_READ);
> +        block_acct_done(stats, &cookie);
> +    }
> +
> +    return NULL;
> +}
> +
> +static void test_latency_histogram_race(void)
> +{
> +    BlockBackend *blk = blk_new(qemu_get_aio_context(),
> +                                BLK_PERM_ALL, BLK_PERM_ALL);
> +    BlockAcctStats *stats = blk_get_stats(blk);
> +    QemuThread writer, readers[NUM_READER_THREADS];
> +    int i;
> +
> +    /* Histogram has to be enabled (bins != NULL) before racing it. */
> +    g_assert(block_latency_histogram_set(stats, BLOCK_ACCT_READ,
> +                                         boundaries_a) == 0);
> +
> +    stop_workers = false;
> +    qemu_thread_create(&writer, "hist-writer", writer_thread, stats,
> +                       QEMU_THREAD_JOINABLE);
> +    for (i = 0; i < NUM_READER_THREADS; i++) {
> +        qemu_thread_create(&readers[i], "hist-reader", reader_thread, stats,
> +                           QEMU_THREAD_JOINABLE);
> +    }
> +
> +    g_usleep(RACE_DURATION_MS * 1000);
> +    qatomic_set(&stop_workers, true);
> +
> +    qemu_thread_join(&writer);
> +    for (i = 0; i < NUM_READER_THREADS; i++) {
> +        qemu_thread_join(&readers[i]);
> +    }
> +
> +    blk_unref(blk);
> +}
> +
> +int main(int argc, char **argv)
> +{
> +    bdrv_init();
> +    qemu_init_main_loop(&error_abort);
> +
> +    g_test_init(&argc, &argv, NULL);
> +
> +    g_test_add_func("/block-accounting/latency_histogram_race",
> +                    test_latency_histogram_race);
> +
> +    return g_test_run();
> +}



^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2026-07-23 14:05 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-23 12:37 [PATCH 0/3] block/accounting: fix unlocked latency histogram reconfiguration races Denis V. Lunev
2026-07-23 12:37 ` [PATCH 1/3] block/accounting: take stats->lock in latency histogram setters Denis V. Lunev
2026-07-23 12:37 ` [PATCH 2/3] block/qapi: take stats->lock when reading BlockAcctStats for query-blockstats Denis V. Lunev
2026-07-23 14:02   ` Andrey Drobyshev
2026-07-23 12:37 ` [PATCH 3/3] tests/unit: add reproducer for BlockAcctStats histogram locking race Denis V. Lunev
2026-07-23 14:04   ` Andrey Drobyshev

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.