From ad95bb54505161696e624646daee4eceec0a9d2e Mon Sep 17 00:00:00 2001 From: Igor Golikov Date: Wed, 27 May 2026 16:19:25 +0300 Subject: [PATCH] client: fix Dentry UAF by holding DentryRef across unlink Hold a function-scoped DentryRef in Client::unlink() to keep the dentry alive through the entire teardown sequence. Without the O_DIRECTORY dentry pin (PR #60909, merged after v19.2.1), opening a directory does not call _ll_get(), so the dentry stays at ref=1 while inode->dir is still active. When trim_cache() evicts this dentry, Dentry::unlink() calls put() for the dir pin, ref drops to 0, and the dentry is freed while Client::unlink() still needs it for detach/lru_remove. A function-scoped DentryRef guarantees the dentry survives until after it has been properly removed from the dir and LRU, regardless of pin state. Signed-off-by: Igor Golikov Fixes: https://tracker.ceph.com/issues/74625 --- src/client/Client.cc | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/client/Client.cc b/src/client/Client.cc index 5d1464b3a3b..af7449e7220 100644 --- a/src/client/Client.cc +++ b/src/client/Client.cc @@ -3791,6 +3791,15 @@ Dentry* Client::link(Dir *dir, const string& name, Inode *in, Dentry *dn) void Client::unlink(Dentry *dn, bool keepdir, bool keepdentry) { InodeRef in(dn->inode); + // Keep the dentry alive for the duration of this function. + // Without the O_DIRECTORY dentry pin (PR #60909), a directory dentry can + // remain at ref=1 while its inode->dir is still active. When trim_cache() + // evicts it, Dentry::unlink() calls put() for the dir pin, dropping ref + // to 0 and freeing the dentry while we still need it for detach/lru_remove. + // The DentryRef guard prevents use-after-free regardless of pin state. + // See: https://tracker.ceph.com/issues/74625 + DentryRef dnref(dn); + ldout(cct, 15) << "unlink dir " << dn->dir->parent_inode << " '" << dn->name << "' dn " << dn << " inode " << dn->inode << dendl; @@ -3800,7 +3809,7 @@ void Client::unlink(Dentry *dn, bool keepdir, bool keepdentry) dec_dentry_nr(); ldout(cct, 20) << "unlink inode " << in << " parents now " << in->dentries << dendl; } - ceph_assert(dn->ref > 0); + ceph_assert(dn->ref > 1); if (keepdentry) { dn->lease_mds = -1; -- 2.47.3