Fix double free when cloning a block with a pending free

dmu_read_l0_bps() captures the source block pointers for a clone
without consulting dnode_block_freed(). A block whose free is recorded
but not yet synced still has a valid on-disk pointer, so the clone
registers a BRT reference to a block about to be returned to the
allocator, and it is freed twice. On a debug build this trips the
metaslab double-free verifier; on a release build the space map is
silently corrupted.

Check for a pending free and return EAGAIN, as dbuf_read_hole() already
does for buffered reads. zfs_clone_range() waits a TXG and retries, by
which point the block reads as a hole.

Two cases need more than the plain check. A hole has nothing to free,
so guard with !BP_IS_HOLE(bp), otherwise a hole inside a pending free
range fails a FICLONE that master cloned. When the head dirty record
already overrode the BP, only frees after the override count against
it, so use dnode_block_freed_after(); dbuf_free_range() leaves the
override in place across a later punch, and cloning the clone would
otherwise capture it.

Reproduced with fstests generic/733, which panicked within a minute on
a debug build, and with a FICLONE of a sparse file in a pending free
range. Both clean after. bclonesaved is correct and scrub is clean.

Suggested-by: mkhllr
Reviewed-by: Tony Hutter <[email protected]>
Reviewed-by: Brian Behlendorf <[email protected]>
Signed-off-by: Patrick Domack <[email protected]>
Closes #18842
Closes #18853
This commit is contained in:
Patrick Domack authored and GitHub committed 2026-09-09 11:08:26 -07:00
1 parent 33ae06bb61
commit 39764eb06b
1 file changed
+38
+38
View File
@@ -2828,6 +2828,44 @@ dmu_read_l0_bps(objset_t *os, uint64_t object, uint64_t offset, uint64_t length,
bp = db->db_blkptr;
}
/*
* A block with a pending free (a truncate or hole
* punch not yet synced) must not be cloned: the clone
* would add a BRT reference to a block about to be
* freed, so the pending clone would apply onto an
* already-freed DVA and double free it. Report
* EAGAIN and let the caller retry once the free
* syncs (the block then reads as a hole).
*
* A hole (or absent BP) has nothing to free and is
* cloned as a hole below. When the head dirty record
* overrode the BP (a clone in this txg) only frees
* recorded after the override count against it;
* earlier frees predate it. This mirrors
* dbuf_read_hole().
*/
if (bp != NULL && !BP_IS_HOLE(bp)) {
dbuf_dirty_record_t *dr =
list_head(&db->db_dirty_records);
boolean_t freed;
DB_DNODE_ENTER(db);
dnode_t *rdn = DB_DNODE(db);
if (dr != NULL && dr->dt.dl.dr_brtwrite) {
freed = dnode_block_freed_after(rdn,
db->db_blkid, dr->dr_txg);
} else {
freed = dnode_block_freed(rdn,
db->db_blkid);
}
DB_DNODE_EXIT(db);
if (freed) {
mutex_exit(&db->db_mtx);
error = SET_ERROR(EAGAIN);
goto out;
}
}
mutex_exit(&db->db_mtx);
if (bp == NULL) {