]> git.proxmox.com Git - mirror_zfs.git/commitdiff
dmu_buf_will_clone: fix race in transition back to NOFILL
authorRob N <robn@despairlabs.com>
Tue, 28 Nov 2023 17:53:04 +0000 (04:53 +1100)
committerGitHub <noreply@github.com>
Tue, 28 Nov 2023 17:53:04 +0000 (09:53 -0800)
Previously, dmu_buf_will_clone() would roll back any dirty record, but
would not clean out the modified data nor reset the state before
releasing the lock. That leaves the last-written data in db_data, but
the dbuf in the wrong state.

This is eventually corrected when the dbuf state is made NOFILL, and
dbuf_noread() called (which clears out the old data), but at this point
its too late, because the lock was already dropped with that invalid
state.

Any caller acquiring the lock before the call into
dmu_buf_will_not_fill() can find what appears to be a clean, readable
buffer, and would take the wrong state from it: it should be getting the
data from the cloned block, not from earlier (unwritten) dirty data.

Even after the state was switched to NOFILL, the old data was still not
cleaned out until dbuf_noread(), which is another gap for a caller to
take the lock and read the wrong data.

This commit fixes all this by properly cleaning up the previous state
and then setting the new state before dropping the lock. The
DBUF_VERIFY() calls confirm that the dbuf is in a valid state when the
lock is down.

Sponsored-by: Klara, Inc.
Sponsored-By: OpenDrives Inc.
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: Pawel Jakub Dawidek <pawel@dawidek.net>
Signed-off-by: Rob Norris <rob.norris@klarasystems.com>
Closes #15566
Closes #15526

module/zfs/dbuf.c

index 5cd97b9faca5dc0ba996d9bab82ecd9ed24f7950..c5ccd4cd1e0c8da1d9ea5363e45a02771ad1c6e0 100644 (file)
@@ -2715,15 +2715,23 @@ dmu_buf_will_clone(dmu_buf_t *db_fake, dmu_tx_t *tx)
         * writes and clones into this block.
         */
        mutex_enter(&db->db_mtx);
+       DBUF_VERIFY(db);
        VERIFY(!dbuf_undirty(db, tx));
        ASSERT0P(dbuf_find_dirty_eq(db, tx->tx_txg));
        if (db->db_buf != NULL) {
                arc_buf_destroy(db->db_buf, db);
                db->db_buf = NULL;
+               dbuf_clear_data(db);
        }
+
+       db->db_state = DB_NOFILL;
+       DTRACE_SET_STATE(db, "allocating NOFILL buffer for clone");
+
+       DBUF_VERIFY(db);
        mutex_exit(&db->db_mtx);
 
-       dmu_buf_will_not_fill(db_fake, tx);
+       dbuf_noread(db);
+       (void) dbuf_dirty(db, tx);
 }
 
 void