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 <batkinson@lanl.gov>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Co-authored-by: gality369 <gality369@example.com>
Signed-off-by: ZhengYuan Huang <gality369@gmail.com>
Closes #18440
This commit is contained in:
Gality
2026-04-21 01:26:28 +08:00
committed by Tony Hutter
parent 7590972f76
commit b8addf9221
+7 -1
View File
@@ -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);
}