btrfs: fix transaction abort on set received ioctl due to item overflow

If the set received ioctl fails due to an item overflow when attempting to
add the BTRFS_UUID_KEY_RECEIVED_SUBVOL we have to abort the transaction
since we did some metadata updates before.

This means that if a user calls this ioctl with the same received UUID
field for a lot of subvolumes, we will hit the overflow, trigger the
transaction abort and turn the filesystem into RO mode. A malicious user
could exploit this, and this ioctl does not even requires that a user
has admin privileges (CAP_SYS_ADMIN), only that he/she owns the subvolume.

Fix this by doing an early check for item overflow before starting a
transaction. This is also race safe because we are holding the subvol_sem
semaphore in exclusive (write) mode.

A test case for fstests will follow soon.

Fixes: dd5f9615fc ("Btrfs: maintain subvolume items in the UUID tree")
CC: stable@vger.kernel.org # 3.12+
Reviewed-by: Anand Jain <asj@kernel.org>
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Reviewed-by: David Sterba <dsterba@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
This commit is contained in:
Filipe Manana
2026-02-26 23:41:07 +00:00
committed by David Sterba
parent e1b18b9590
commit 87f2c46003
3 changed files with 59 additions and 2 deletions

View File

@@ -3929,6 +3929,25 @@ static long _btrfs_ioctl_set_received_subvol(struct file *file,
goto out;
}
received_uuid_changed = memcmp(root_item->received_uuid, sa->uuid,
BTRFS_UUID_SIZE);
/*
* Before we attempt to add the new received uuid, check if we have room
* for it in case there's already an item. If the size of the existing
* item plus this root's ID (u64) exceeds the maximum item size, we can
* return here without the need to abort a transaction. If we don't do
* this check, the btrfs_uuid_tree_add() call below would fail with
* -EOVERFLOW and result in a transaction abort. Malicious users could
* exploit this to turn the fs into RO mode.
*/
if (received_uuid_changed && !btrfs_is_empty_uuid(sa->uuid)) {
ret = btrfs_uuid_tree_check_overflow(fs_info, sa->uuid,
BTRFS_UUID_KEY_RECEIVED_SUBVOL);
if (ret < 0)
goto out;
}
/*
* 1 - root item
* 2 - uuid items (received uuid + subvol uuid)
@@ -3944,8 +3963,6 @@ static long _btrfs_ioctl_set_received_subvol(struct file *file,
sa->rtime.sec = ct.tv_sec;
sa->rtime.nsec = ct.tv_nsec;
received_uuid_changed = memcmp(root_item->received_uuid, sa->uuid,
BTRFS_UUID_SIZE);
if (received_uuid_changed &&
!btrfs_is_empty_uuid(root_item->received_uuid)) {
ret = btrfs_uuid_tree_remove(trans, root_item->received_uuid,

View File

@@ -199,6 +199,44 @@ int btrfs_uuid_tree_remove(struct btrfs_trans_handle *trans, const u8 *uuid, u8
return 0;
}
/*
* Check if we can add one root ID to a UUID key.
* If the key does not yet exists, we can, otherwise only if extended item does
* not exceeds the maximum item size permitted by the leaf size.
*
* Returns 0 on success, negative value on error.
*/
int btrfs_uuid_tree_check_overflow(struct btrfs_fs_info *fs_info,
const u8 *uuid, u8 type)
{
BTRFS_PATH_AUTO_FREE(path);
int ret;
u32 item_size;
struct btrfs_key key;
if (WARN_ON_ONCE(!fs_info->uuid_root))
return -EINVAL;
path = btrfs_alloc_path();
if (!path)
return -ENOMEM;
btrfs_uuid_to_key(uuid, type, &key);
ret = btrfs_search_slot(NULL, fs_info->uuid_root, &key, path, 0, 0);
if (ret < 0)
return ret;
if (ret > 0)
return 0;
item_size = btrfs_item_size(path->nodes[0], path->slots[0]);
if (sizeof(struct btrfs_item) + item_size + sizeof(u64) >
BTRFS_LEAF_DATA_SIZE(fs_info))
return -EOVERFLOW;
return 0;
}
static int btrfs_uuid_iter_rem(struct btrfs_root *uuid_root, u8 *uuid, u8 type,
u64 subid)
{

View File

@@ -12,6 +12,8 @@ int btrfs_uuid_tree_add(struct btrfs_trans_handle *trans, const u8 *uuid, u8 typ
u64 subid);
int btrfs_uuid_tree_remove(struct btrfs_trans_handle *trans, const u8 *uuid, u8 type,
u64 subid);
int btrfs_uuid_tree_check_overflow(struct btrfs_fs_info *fs_info,
const u8 *uuid, u8 type);
int btrfs_uuid_tree_iterate(struct btrfs_fs_info *fs_info);
int btrfs_create_uuid_tree(struct btrfs_fs_info *fs_info);
int btrfs_uuid_scan_kthread(void *data);