From b8addf92216a358d3936092f88bb916c64ce3e20 Mon Sep 17 00:00:00 2001 From: Gality <68463495+Gality369@users.noreply.github.com> Date: Tue, 21 Apr 2026 01:26:28 +0800 Subject: [PATCH] dmu_direct: avoid UAF in dmu_write_direct_done() dmu_write_direct_done() passes dmu_sync_arg_t to dmu_sync_done(), which updates the override state and frees the completion context. The Direct I/O error path then still dereferences dsa->dsa_tx while rolling the dirty record back with dbuf_undirty(), resulting in a use-after-free. Save dsa->dsa_tx in a local variable before calling dmu_sync_done() and use that saved tx for the error rollback. This preserves the existing ownership model for dsa and does not change the Direct I/O write semantics. Reviewed-by: Brian Atkinson Reviewed-by: Brian Behlendorf Co-authored-by: gality369 Signed-off-by: ZhengYuan Huang Closes #18440 --- module/zfs/dmu_direct.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/module/zfs/dmu_direct.c b/module/zfs/dmu_direct.c index d44c68608..5b00698da 100644 --- a/module/zfs/dmu_direct.c +++ b/module/zfs/dmu_direct.c @@ -91,6 +91,7 @@ dmu_write_direct_done(zio_t *zio) dmu_sync_arg_t *dsa = zio->io_private; dbuf_dirty_record_t *dr = dsa->dsa_dr; dmu_buf_impl_t *db = dr->dr_dbuf; + dmu_tx_t *tx = dsa->dsa_tx; abd_free(zio->io_abd); @@ -101,6 +102,11 @@ dmu_write_direct_done(zio_t *zio) db->db_state = DB_UNCACHED; mutex_exit(&db->db_mtx); + /* + * dmu_sync_done() owns dsa and frees it after publishing the final + * override state. The direct-I/O error path still needs the original + * open-context tx to roll the dirty record back with dbuf_undirty(). + */ dmu_sync_done(zio, NULL, zio->io_private); if (zio->io_error != 0) { @@ -120,7 +126,7 @@ dmu_write_direct_done(zio_t *zio) * calling dbuf_undirty(). */ mutex_enter(&db->db_mtx); - VERIFY3B(dbuf_undirty(db, dsa->dsa_tx), ==, B_FALSE); + VERIFY3B(dbuf_undirty(db, tx), ==, B_FALSE); mutex_exit(&db->db_mtx); }