From 253612d4727a92f5c11d1b1d8fa7801c8952866d Mon Sep 17 00:00:00 2001 From: Matej Kenda Date: Mon, 17 Aug 2026 22:20:40 +0200 Subject: [PATCH 1/3] fix: releases of unheld meta_lock in rename and unlink error paths The WORM and non-empty-directory checks ran before the lock was taken but their error paths released it. Take it first; lock order is unchanged. --- src/libltfs/ltfs_fsops.c | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/libltfs/ltfs_fsops.c b/src/libltfs/ltfs_fsops.c index 3b7f57d6..8e7d8e53 100644 --- a/src/libltfs/ltfs_fsops.c +++ b/src/libltfs/ltfs_fsops.c @@ -460,6 +460,9 @@ int ltfs_fsops_unlink(const char *path, ltfs_file_id *id, struct ltfs_volume *vo } parent = d->parent; + /* Lock order: parent contents_lock, parent meta_lock, then child meta_lock */ + acquirewrite_mrsw(&parent->meta_lock); + if (parent->is_immutable || parent->is_appendonly) { ltfsmsg(LTFS_ERR, 17237E, "unlink: parent is WORM"); ret = -LTFS_WORM_ENABLED; @@ -482,7 +485,6 @@ int ltfs_fsops_unlink(const char *path, ltfs_file_id *id, struct ltfs_volume *vo goto out; } - acquirewrite_mrsw(&parent->meta_lock); acquirewrite_mrsw(&d->meta_lock); if (dcache_initialized(vol)) { @@ -640,19 +642,6 @@ int ltfs_fsops_rename(const char *from, const char *to, ltfs_file_id *id, struct goto out_release; } - if (fromdir->is_appendonly || fromdir->is_immutable ) { - ltfsmsg(LTFS_ERR, 17237E, "rename: parent is WORM"); - ret = -LTFS_WORM_ENABLED; - acquirewrite_mrsw(&fromdir->meta_lock); - goto out_release; - } - if (todir->is_immutable || todir->is_appendonly) { - ltfsmsg(LTFS_ERR, 17237E, "rename: target dir is WORM"); - ret = -LTFS_WORM_ENABLED; - acquirewrite_mrsw(&fromdir->meta_lock); - goto out_release; - } - /* Take locks in the appropriate order and look up the source and destination dentries */ if (todir == fromdir || fs_is_predecessor(todir, fromdir)) { acquirewrite_mrsw(&todir->contents_lock); @@ -760,6 +749,16 @@ int ltfs_fsops_rename(const char *from, const char *to, ltfs_file_id *id, struct } #endif + if (fromdir->is_immutable || fromdir->is_appendonly || + todir->is_immutable || todir->is_appendonly) { + ltfsmsg(LTFS_ERR, 17237E, "rename: source or target dir is WORM"); + ret = -LTFS_WORM_ENABLED; + fs_release_dentry(fromdentry); + if (todentry && todentry != fromdentry) + fs_release_dentry(todentry); + goto out_unlock; + } + if (fromdentry->is_immutable || fromdentry->is_appendonly) { ltfsmsg(LTFS_ERR, 17237E, "rename: src entry is WORM"); ret = -LTFS_WORM_ENABLED; From 3800dcb00a5d85901beaa4bc1f15a18194087c11 Mon Sep 17 00:00:00 2001 From: Matej Kenda Date: Mon, 17 Aug 2026 22:20:40 +0200 Subject: [PATCH 2/3] fix: double free of the new name on a late rename failure After ownership moves to fromdentry, out_free freed the buffers again. --- src/libltfs/ltfs_fsops.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libltfs/ltfs_fsops.c b/src/libltfs/ltfs_fsops.c index 8e7d8e53..d1fbcbfc 100644 --- a/src/libltfs/ltfs_fsops.c +++ b/src/libltfs/ltfs_fsops.c @@ -856,6 +856,9 @@ int ltfs_fsops_rename(const char *from, const char *to, ltfs_file_id *id, struct fromdentry->name.percent_encode = fs_is_percent_encode_required(fromdentry->name.name); fromdentry->platform_safe_name = to_filename_copy2; fromdentry->matches_name_criteria = index_criteria_match(fromdentry, vol); + /* fromdentry owns the buffers now; keep out_free from freeing them again */ + to_filename_copy = NULL; + to_filename_copy2 = NULL; /* Add fromdentry to new directory */ todir->child_list = fs_add_key_to_hash_table(todir->child_list, fromdentry, &ret); From f9aa71c629bc717b5d467398088106e4f4a0886f Mon Sep 17 00:00:00 2001 From: Matej Kenda Date: Wed, 19 Aug 2026 12:05:15 +0200 Subject: [PATCH 3/3] fix: check for a non-empty directory before taking parent->meta_lock in unlink The check needs only the child's contents_lock, so parent->meta_lock is no longer held while it is taken. --- src/libltfs/ltfs_fsops.c | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/libltfs/ltfs_fsops.c b/src/libltfs/ltfs_fsops.c index d1fbcbfc..f14d8954 100644 --- a/src/libltfs/ltfs_fsops.c +++ b/src/libltfs/ltfs_fsops.c @@ -460,6 +460,23 @@ int ltfs_fsops_unlink(const char *path, ltfs_file_id *id, struct ltfs_volume *vo } parent = d->parent; + /* Can't remove non-empty directories */ + if (d->isdir) { + ret = 0; + acquireread_mrsw(&d->contents_lock); + if (HASH_COUNT(d->child_list) != 0) + ret = -LTFS_DIRNOTEMPTY; + releaseread_mrsw(&d->contents_lock); + if (ret < 0) { + releasewrite_mrsw(&parent->contents_lock); + fs_release_dentry(parent); + releaseread_mrsw(&vol->lock); + free(path_norm); + fs_release_dentry(d); + return ret; + } + } + /* Lock order: parent contents_lock, parent meta_lock, then child meta_lock */ acquirewrite_mrsw(&parent->meta_lock); @@ -474,17 +491,6 @@ int ltfs_fsops_unlink(const char *path, ltfs_file_id *id, struct ltfs_volume *vo goto out; } - /* Can't remove non-empty directories */ - if (d->isdir) { - ret = 0; - acquireread_mrsw(&d->contents_lock); - if (HASH_COUNT(d->child_list) != 0) - ret = -LTFS_DIRNOTEMPTY; - releaseread_mrsw(&d->contents_lock); - if (ret < 0) - goto out; - } - acquirewrite_mrsw(&d->meta_lock); if (dcache_initialized(vol)) {