btrfs-progs/libbtrfsutil/python/tests/__init__.py
Omar Sandoval e9c2942f38 libbtrfsutil: fix race between subvolume iterator and deletion
Subvolume iteration has a window between when we get a root ref (with
BTRFS_IOC_TREE_SEARCH or BTRFS_IOC_GET_SUBVOL_ROOTREF) and when we look
up the path of the parent directory (with BTRFS_IOC_INO_LOOKUP{,_USER}).
If the subvolume is moved or deleted and its old parent directory is
deleted during that window, then BTRFS_IOC_INO_LOOKUP{,_USER} will fail
with ENOENT. The iteration will then fail with ENOENT as well.

We originally encountered this bug with an application that called
`btrfs subvolume show` (which iterates subvolumes to find snapshots) in
parallel with other threads creating and deleting subvolumes. It can be
reproduced almost instantly with the included test cases.

Subvolume iteration should be robust against concurrent modifications to
subvolumes. So, if a subvolume's parent directory no longer exists, just
skip the subvolume, as it must have been deleted or moved elsewhere.

Reviewed-by: Neal Gompa <ngompa13@gmail.com>
Signed-off-by: Omar Sandoval <osandov@fb.com>
Signed-off-by: David Sterba <dsterba@suse.com>
2021-07-29 13:01:55 +02:00

118 lines
3.1 KiB
Python

# Copyright (C) 2018 Facebook
#
# This file is part of libbtrfsutil.
#
# libbtrfsutil is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# libbtrfsutil is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with libbtrfsutil. If not, see <http://www.gnu.org/licenses/>.
import contextlib
import os
from pathlib import PurePath
import pwd
import subprocess
import tempfile
import unittest
HAVE_PATH_LIKE = hasattr(PurePath, '__fspath__')
try:
NOBODY_UID = pwd.getpwnam('nobody').pw_uid
skipUnlessHaveNobody = lambda func: func
except KeyError:
NOBODY_UID = None
skipUnlessHaveNobody = unittest.skip('must have nobody user')
@contextlib.contextmanager
def drop_privs():
try:
os.seteuid(NOBODY_UID)
yield
finally:
os.seteuid(0)
@contextlib.contextmanager
def regain_privs():
uid = os.geteuid()
if uid:
try:
os.seteuid(0)
yield
finally:
os.seteuid(uid)
else:
yield
@unittest.skipIf(os.geteuid() != 0, 'must be run as root')
class BtrfsTestCase(unittest.TestCase):
def __init__(self, *args, **kwds):
super().__init__(*args, **kwds)
self._mountpoints = []
def mount_btrfs(self):
mountpoint = tempfile.mkdtemp()
try:
with tempfile.NamedTemporaryFile(delete=False) as f:
os.truncate(f.fileno(), 1024 * 1024 * 1024)
image = f.name
except Exception as e:
os.rmdir(mountpoint)
raise e
if os.path.exists('../../mkfs.btrfs'):
mkfs = '../../mkfs.btrfs'
else:
mkfs = 'mkfs.btrfs'
try:
subprocess.check_call([mkfs, '-q', image])
subprocess.check_call(
[
'mount',
'-o',
'loop,user_subvol_rm_allowed',
'--',
image,
mountpoint,
]
)
except Exception as e:
os.rmdir(mountpoint)
os.remove(image)
raise e
self._mountpoints.append((mountpoint, image))
return mountpoint, image
def setUp(self):
self.mountpoint, self.image = self.mount_btrfs()
def tearDown(self):
for mountpoint, image in self._mountpoints:
subprocess.call(['umount', '-R', mountpoint])
os.rmdir(mountpoint)
os.remove(image)
@staticmethod
def path_or_fd(path, open_flags=os.O_RDONLY):
yield path
yield path.encode()
if HAVE_PATH_LIKE:
yield PurePath(path)
fd = os.open(path, open_flags)
try:
yield fd
finally:
os.close(fd)