Skip to contentSkip to conversation

ceph_encode_dentry_release() misuse of dget()

deadlock or dead code Al Viro 3 participants 5 messages Nov 16, 2023 – Nov 17, 2023 VFS

lore
Share & tools
lore

Al Viro

author maintainer
#0 lore raw

This

        spin_lock(&dentry->d_lock);
        if (di->lease_session && di->lease_session->s_mds == mds)
                force = 1;
        if (!dir) {
                parent = dget(dentry->d_parent);
                dir = d_inode(parent);
        }
        spin_unlock(&dentry->d_lock);

has a problem if we ever get called with dir == NULL.

The thing is, dget(dentry->d_parent) will spin if dentry->d_parent->d_lock is held. IOW, the whole thing is an equivalent of

	spin_lock(&dentry->d_lock);
	spin_lock(&dentry->d_parent->d_lock);

which takes them in the wrong order.

Said that, I'm not sure it ever gets called with dir == NULL; we have two callers -

        if (req->r_dentry_drop) {
                ret = ceph_encode_dentry_release(&p, req->r_dentry,
                                req->r_parent, mds, req->r_dentry_drop,
                                req->r_dentry_unless);
                if (ret < 0)
                        goto out_err;
                releases += ret;
        }
and
        if (req->r_old_dentry_drop) {
                ret = ceph_encode_dentry_release(&p, req->r_old_dentry,
                                req->r_old_dentry_dir, mds,
                                req->r_old_dentry_drop,
                                req->r_old_dentry_unless);
                if (ret < 0)
                        goto out_err;
                releases += ret;
        }
Now, ->r_dentry_drop is set in ceph_mknod(), ceph_symlink(), ceph_mkdir(),
ceph_link(), ceph_unlink(), all with
        req->r_parent = dir;
        ihold(dir);
ceph_rename(), with
        req->r_parent = new_dir;
        ihold(new_dir);
and ceph_atomic_open(), with
        req->r_parent = dir;
        if (req->r_op == CEPH_MDS_OP_CREATE)
                req->r_mnt_idmap = mnt_idmap_get(idmap);
        ihold(dir);
All of that will oops if ->r_parent set to NULL.
->r_old_dentry_drop() is set in ceph_rename(), with
        struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(old_dir->i_sb);
	...
        req->r_old_dentry_dir = old_dir;

which also can't manage to leave ->r_old_dentry_dir set to NULL.

Am I missing something subtle here? Looks like that dget() is never reached...

#1 lore raw
Quoted 11 lines · Al Viro

This

        spin_lock(&dentry->d_lock);
        if (di->lease_session && di->lease_session->s_mds == mds)
                force = 1;
        if (!dir) {
                parent = dget(dentry->d_parent);
                dir = d_inode(parent);
        }
        spin_unlock(&dentry->d_lock);

has a problem if we ever get called with dir == NULL.

Ouch, ok.

Quoted 43 lines

The thing is, dget(dentry->d_parent) will spin if dentry->d_parent->d_lock is held. IOW, the whole thing is an equivalent of

	spin_lock(&dentry->d_lock);
	spin_lock(&dentry->d_parent->d_lock);

which takes them in the wrong order.

Said that, I'm not sure it ever gets called with dir == NULL; we have two callers -

        if (req->r_dentry_drop) {
                ret = ceph_encode_dentry_release(&p, req->r_dentry,
                                req->r_parent, mds, req->r_dentry_drop,
                                req->r_dentry_unless);
                if (ret < 0)
                        goto out_err;
                releases += ret;
        }
and
        if (req->r_old_dentry_drop) {
                ret = ceph_encode_dentry_release(&p, req->r_old_dentry,
                                req->r_old_dentry_dir, mds,
                                req->r_old_dentry_drop,
                                req->r_old_dentry_unless);
                if (ret < 0)
                        goto out_err;
                releases += ret;
        }
Now, ->r_dentry_drop is set in ceph_mknod(), ceph_symlink(), ceph_mkdir(),
ceph_link(), ceph_unlink(), all with
        req->r_parent = dir;
        ihold(dir);
ceph_rename(), with
        req->r_parent = new_dir;
        ihold(new_dir);
and ceph_atomic_open(), with
        req->r_parent = dir;
        if (req->r_op == CEPH_MDS_OP_CREATE)
                req->r_mnt_idmap = mnt_idmap_get(idmap);
        ihold(dir);
All of that will oops if ->r_parent set to NULL.
->r_old_dentry_drop() is set in ceph_rename(), with
        struct ceph_mds_client *mdsc = ceph_sb_to_mdsc(old_dir->i_sb);
	...
        req->r_old_dentry_dir = old_dir;
Quoted 4 lines

which also can't manage to leave ->r_old_dentry_dir set to NULL.

Am I missing something subtle here? Looks like that dget() is never reached...

No, I think you're correct. That looks like dead code to me too. Probably we can just remove that "if (!dir)" condition altogether.

Did you want to send a patch, or would you rather Xiubo or I do it?

Thanks,

Signature · 1 line
Jeff Layton <[email protected]>
author maintainer
#2 lore raw
Quoted 7 lines · Jeff Layton
Quoted 2 lines

Am I missing something subtle here? Looks like that dget() is never reached...

No, I think you're correct. That looks like dead code to me too. Probably we can just remove that "if (!dir)" condition altogether.

Did you want to send a patch, or would you rather Xiubo or I do it?

Up to you... AFAICS, it had been dead code since ca6c8ae0f793 "ceph: pass parent inode info to ceph_encode_dentry_release if we have it". In other words, that "if we have it" had already been true at that point. Prior to that commit dget() in there had been unconditional (and really a deadlock fodder); making it conditional had made it actually unreachable and that fixed the actual bug.

#3 lore raw
Quoted 8 lines · Al Viro
Quoted 6 lines · Jeff Layton
Quoted 2 lines

Am I missing something subtle here? Looks like that dget() is never reached...

No, I think you're correct. That looks like dead code to me too. Probably we can just remove that "if (!dir)" condition altogether.

Quoted 8 lines
Quoted 1 line

Did you want to send a patch, or would you rather Xiubo or I do it?

Up to you... AFAICS, it had been dead code since ca6c8ae0f793 "ceph: pass parent inode info to ceph_encode_dentry_release if we have it". In other words, that "if we have it" had already been true at that point. Prior to that commit dget() in there had been unconditional (and really a deadlock fodder); making it conditional had made it actually unreachable and that fixed the actual bug.

That makes sense.

Xiubo, would you mind spinning up a patch for this? You're probably in the best position to make sure it gets tested these days.

Thanks!

Signature · 1 line
Jeff Layton <[email protected]>
#4 lore raw
Quoted 15 lines · Jeff Layton
Quoted 7 lines · Al Viro
Quoted 5 lines · Jeff Layton
Quoted 2 lines

Am I missing something subtle here? Looks like that dget() is never reached...

No, I think you're correct. That looks like dead code to me too. Probably we can just remove that "if (!dir)" condition altogether.

Quoted 7 lines
Quoted 1 line

Did you want to send a patch, or would you rather Xiubo or I do it?

Up to you... AFAICS, it had been dead code since ca6c8ae0f793 "ceph: pass parent inode info to ceph_encode_dentry_release if we have it". In other words, that "if we have it" had already been true at that point. Prior to that commit dget() in there had been unconditional (and really a deadlock fodder); making it conditional had made it actually unreachable and that fixed the actual bug.

Quoted 4 lines

That makes sense.

Xiubo, would you mind spinning up a patch for this? You're probably in the best position to make sure it gets tested these days.

Hi Jeff, Al

Sure, I will fix this. Thanks very much.

- Xiubo

Quoted 1 line

Thanks!