mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-28 11:25:21 +01:00
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:
1 parent
33ae06bb61
commit
39764eb06b
1 file changed
+38
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user