QStorageInfo/Linux: use the mount ID to match paths to mountinfo lines

Linux kernel version 5.8 added support for the stx_mnt_id field in the
struct statx because "Systemd is hacking around to get it and it's
trivial to add to statx, so...". This allows us to much more neatly
match the lines in /proc/self/mountinfo.

The same kernel version added STATX_ATTR_MOUNT_ROOT so we can tell if a
given path is the mount point of a filesystem. We don't have a need for
that information for now.

We need to retain fallback code for two reasons: first, the user may be
running with an old Linux kernel, in which case we won't get the
STATX_MNT_ID bit set in stx_mask. Second, we may have failed to open()
the path in question, because the user may not have the necessary
permissions.

There's still a race condition because the mount IDs can be reused
immediately after something is unmounted. There's a 64-bit unique mount
ID (available since v6.8) but it's not reported in /proc/self/mountinfo,
so we couldn't us it right now. We can with 6.8's statmount().

Pick-to: 6.7
Task-number: QTBUG-125721
Change-Id: If3345151ddf84c43a4f1fffd17d3f7dbce4ff16b
Reviewed-by: Ahmad Samir <a.samirh78@gmail.com>
This commit is contained in:
Thiago Macieira 2024-05-29 10:01:55 -03:00
parent ad968d3602
commit 8836b67778
4 changed files with 113 additions and 49 deletions

View File

@ -13,6 +13,7 @@
#include <q20memory.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/statfs.h>
// so we don't have to #include <linux/fs.h>, which is known to cause conflicts
@ -28,10 +29,29 @@
# define ST_RDONLY 0x0001 /* mount read-only */
#endif
#if defined(Q_OS_ANDROID)
// statx() is disabled on Android because quite a few systems
// come with sandboxes that kill applications that make system calls outside a
// whitelist and several Android vendors can't be bothered to update the list.
# undef STATX_BASIC_STATS
#endif
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
namespace {
struct AutoFileDescriptor
{
int fd = -1;
AutoFileDescriptor(const QString &path, int mode = QT_OPEN_RDONLY)
: fd(qt_safe_open(QFile::encodeName(path), mode))
{}
~AutoFileDescriptor() { if (fd >= 0) qt_safe_close(fd); }
operator int() const noexcept { return fd; }
};
}
// udev encodes the labels with ID_LABEL_FS_ENC which is done with
// blkid_encode_string(). Within this function some 1-byte utf-8
// characters not considered safe (e.g. '\' or ' ') are encoded as hex
@ -79,6 +99,20 @@ static inline dev_t deviceIdForPath(const QString &device)
return st.st_dev;
}
static inline quint64 mountIdForPath(int fd)
{
if (fd < 0)
return 0;
#if defined(STATX_BASIC_STATS) && defined(STATX_MNT_ID)
// STATX_MNT_ID was added in kernel v5.8
struct statx st;
int r = statx(fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, STATX_MNT_ID, &st);
if (r == 0 && (st.stx_mask & STATX_MNT_ID))
return st.stx_mnt_id;
#endif
return 0;
}
static inline quint64 retrieveDeviceId(const QByteArray &device, quint64 deviceId = 0)
{
// major = 0 implies an anonymous block device, so we need to stat() the
@ -207,33 +241,45 @@ void QStorageInfoPrivate::doStat()
return;
}
// We iterate over the /proc/self/mountinfo list backwards because then any
// matching isParentOf must be the actual mount point because it's the most
// recent mount on that path. Linux does allow mounting over non-empty
// directories, such as in:
// # mount | tail -2
// tmpfs on /tmp/foo/bar type tmpfs (rw,relatime,inode64)
// tmpfs on /tmp/foo type tmpfs (rw,relatime,inode64)
//
// We try to match the device ID in case there's a mount --move.
// We can't *rely* on it because some filesystems like btrfs will assign
// device IDs to subvolumes that aren't listed in /proc/self/mountinfo.
const QString oldRootPath = std::exchange(rootPath, QString());
const dev_t rootPathDevId = deviceIdForPath(oldRootPath);
MountInfo *best = nullptr;
for (auto it = infos.rbegin(); it != infos.rend(); ++it) {
if (!isParentOf(it->mountPoint, oldRootPath))
continue;
if (rootPathDevId == it->stDev) {
// device ID matches; this is definitely the best option
best = q20::to_address(it);
break;
}
if (!best) {
// if we can't find a device ID match, this parent path is probably
// the correct one
AutoFileDescriptor fd(rootPath);
if (quint64 mntid = mountIdForPath(fd)) {
// We have the mount ID for this path, so find the matching line.
auto it = std::find_if(infos.begin(), infos.end(),
[mntid](const MountInfo &info) { return info.mntid == mntid; });
if (it != infos.end())
best = q20::to_address(it);
} else {
// We have failed to get the mount ID for this path, usually because
// the path cannot be open()ed by this user (e.g., /root), so we fall
// back to a string search.
// We iterate over the /proc/self/mountinfo list backwards because then any
// matching isParentOf must be the actual mount point because it's the most
// recent mount on that path. Linux does allow mounting over non-empty
// directories, such as in:
// # mount | tail -2
// tmpfs on /tmp/foo/bar type tmpfs (rw,relatime,inode64)
// tmpfs on /tmp/foo type tmpfs (rw,relatime,inode64)
//
// We try to match the device ID in case there's a mount --move.
// We can't *rely* on it because some filesystems like btrfs will assign
// device IDs to subvolumes that aren't listed in /proc/self/mountinfo.
const QString oldRootPath = std::exchange(rootPath, QString());
const dev_t rootPathDevId = deviceIdForPath(oldRootPath);
for (auto it = infos.rbegin(); it != infos.rend(); ++it) {
if (!isParentOf(it->mountPoint, oldRootPath))
continue;
if (rootPathDevId == it->stDev) {
// device ID matches; this is definitely the best option
best = q20::to_address(it);
break;
}
if (!best) {
// if we can't find a device ID match, this parent path is probably
// the correct one
best = q20::to_address(it);
}
}
}
if (best) {
@ -274,12 +320,21 @@ QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
volumes.reserve(infos.size());
for (auto it = infos.begin(); it != infos.end(); ++it) {
MountInfo &info = *it;
// Scan the later lines to see if any is a parent to this
auto isParent = [&info](const MountInfo &maybeParent) {
return isParentOf(maybeParent.mountPoint, info.mountPoint);
};
if (std::find_if(it + 1, infos.end(), isParent) != infos.end())
AutoFileDescriptor fd(info.mountPoint);
// find out if the path as we see it matches this line from mountinfo
quint64 mntid = mountIdForPath(fd);
if (mntid == 0) {
// statx failed, so scan the later lines to see if any is a parent
// to this
auto isParent = [&info](const MountInfo &maybeParent) {
return isParentOf(maybeParent.mountPoint, info.mountPoint);
};
if (std::find_if(it + 1, infos.end(), isParent) != infos.end())
continue;
} else if (mntid != info.mntid) {
continue;
}
const auto infoStDev = info.stDev;
QStorageInfoPrivate d(std::move(info));

View File

@ -93,7 +93,7 @@ static QByteArray parseMangledPath(QByteArrayView path)
}
// Indexes into the "fields" std::array in parseMountInfo()
// static constexpr short MountId = 0;
static constexpr short MountId = 0;
// static constexpr short ParentId = 1;
static constexpr short DevNo = 2;
static constexpr short FsRoot = 3;
@ -199,6 +199,13 @@ doParseMountInfo(const QByteArray &mountinfo, FilterMountInfo filter = FilterMou
tokenizeLine(fields, line);
MountInfo info;
if (auto r = qstrntoll(fields[MountId].data(), fields[MountId].size(), 10); r.ok()) {
info.mntid = r.result;
} else {
checkField({});
continue;
}
QByteArray mountP = parseMangledPath(fields[MountPoint]);
if (!checkField(mountP))
continue;

View File

@ -69,6 +69,7 @@ public:
QByteArray device;
QByteArray fsRoot;
dev_t stDev = 0;
quint64 mntid = 0;
};
void setFromMountInfo(MountInfo &&info)

View File

@ -319,64 +319,64 @@ void tst_QStorageInfo::testParseMountInfo_data()
QTest::newRow("tmpfs")
<< "17 25 0:18 / /dev rw,nosuid,relatime shared:2 - tmpfs tmpfs rw,seclabel,mode=755\n"_ba
<< MountInfo{"/dev", "tmpfs", "tmpfs", "", makedev(0, 18)};
<< MountInfo{"/dev", "tmpfs", "tmpfs", "", makedev(0, 18), 17};
QTest::newRow("proc")
<< "23 66 0:21 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw\n"_ba
<< MountInfo{"/proc", "proc", "proc", "", makedev(0, 21)};
<< MountInfo{"/proc", "proc", "proc", "", makedev(0, 21), 23};
// E.g. on Android
QTest::newRow("rootfs")
<< "618 618 0:1 / / ro,relatime master:1 - rootfs rootfs ro,seclabel\n"_ba
<< MountInfo{"/", "rootfs", "rootfs", "", makedev(0, 1)};
<< MountInfo{"/", "rootfs", "rootfs", "", makedev(0, 1), 618};
QTest::newRow("ext4")
<< "47 66 8:3 / /home rw,relatime shared:50 - ext4 /dev/sda3 rw,stripe=32736\n"_ba
<< MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3)};
<< MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3), 47};
QTest::newRow("empty-optional-field")
<< "23 25 0:22 / /apex rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,seclabel,mode=755\n"_ba
<< MountInfo{"/apex", "tmpfs", "tmpfs", "", makedev(0, 22)};
<< MountInfo{"/apex", "tmpfs", "tmpfs", "", makedev(0, 22), 23};
QTest::newRow("one-optional-field")
<< "47 66 8:3 / /home rw,relatime shared:50 - ext4 /dev/sda3 rw,stripe=32736\n"_ba
<< MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3)};
<< MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3), 47};
QTest::newRow("multiple-optional-fields")
<< "47 66 8:3 / /home rw,relatime shared:142 master:111 - ext4 /dev/sda3 rw,stripe=32736\n"_ba
<< MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3)};
<< MountInfo{"/home", "ext4", "/dev/sda3", "", makedev(8, 3), 47};
QTest::newRow("mountdir-with-utf8")
<< "129 66 8:51 / /mnt/lab\xC3\xA9l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba
<< MountInfo{"/mnt/labél", "ext4", "/dev/sdd3", "", makedev(8, 51)};
<< MountInfo{"/mnt/labél", "ext4", "/dev/sdd3", "", makedev(8, 51), 129};
QTest::newRow("mountdir-with-space")
<< "129 66 8:51 / /mnt/labe\\040l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba
<< MountInfo{"/mnt/labe l", "ext4", "/dev/sdd3", "", makedev(8, 51)};
<< MountInfo{"/mnt/labe l", "ext4", "/dev/sdd3", "", makedev(8, 51), 129};
QTest::newRow("mountdir-with-tab")
<< "129 66 8:51 / /mnt/labe\\011l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba
<< MountInfo{"/mnt/labe\tl", "ext4", "/dev/sdd3", "", makedev(8, 51)};
<< MountInfo{"/mnt/labe\tl", "ext4", "/dev/sdd3", "", makedev(8, 51), 129};
QTest::newRow("mountdir-with-backslash")
<< "129 66 8:51 / /mnt/labe\\134l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba
<< MountInfo{"/mnt/labe\\l", "ext4", "/dev/sdd3", "", makedev(8, 51)};
<< MountInfo{"/mnt/labe\\l", "ext4", "/dev/sdd3", "", makedev(8, 51), 129};
QTest::newRow("mountdir-with-newline")
<< "129 66 8:51 / /mnt/labe\\012l rw,relatime shared:234 - ext4 /dev/sdd3 rw\n"_ba
<< MountInfo{"/mnt/labe\nl", "ext4", "/dev/sdd3", "", makedev(8, 51)};
<< MountInfo{"/mnt/labe\nl", "ext4", "/dev/sdd3", "", makedev(8, 51), 129};
QTest::newRow("btrfs-subvol")
<< "775 503 0:49 /foo/bar / rw,relatime shared:142 master:111 - btrfs "
"/dev/mapper/vg0-stuff rw,ssd,discard,space_cache,subvolid=272,subvol=/foo/bar\n"_ba
<< MountInfo{"/", "btrfs", "/dev/mapper/vg0-stuff", "/foo/bar", makedev(0, 49)};
<< MountInfo{"/", "btrfs", "/dev/mapper/vg0-stuff", "/foo/bar", makedev(0, 49), 775};
QTest::newRow("bind-mount")
<< "59 47 8:17 /rpmbuild /home/user/rpmbuild rw,relatime shared:48 - ext4 /dev/sdb1 rw\n"_ba
<< MountInfo{"/home/user/rpmbuild", "ext4", "/dev/sdb1", "/rpmbuild", makedev(8, 17)};
<< MountInfo{"/home/user/rpmbuild", "ext4", "/dev/sdb1", "/rpmbuild", makedev(8, 17), 59};
QTest::newRow("space-dash-space")
<< "47 66 8:3 / /home\\040-\\040dir rw,relatime shared:50 - ext4 /dev/sda3 rw,stripe=32736\n"_ba
<< MountInfo{"/home - dir", "ext4", "/dev/sda3", "", makedev(8, 3)};
<< MountInfo{"/home - dir", "ext4", "/dev/sda3", "", makedev(8, 3), 47};
QTest::newRow("btrfs-mount-bind-file")
<< "1799 1778 0:49 "
@ -385,7 +385,7 @@ void tst_QStorageInfo::testParseMountInfo_data()
"rw,ssd,discard,space_cache,subvolid=1773,subvol=/var_lib_docker\n"_ba
<< MountInfo{"/etc/resolv.conf", "btrfs", "/dev/mapper/vg0-stuff",
"/var_lib_docker/containers/81fde0fec3dd3d99765c3f7fd9cf1ab121b6ffcfd05d5d7ff434db933fe9d795/resolv.conf",
makedev(0, 49)};
makedev(0, 49), 1799};
QTest::newRow("very-long-line-QTBUG-77059")
<< "727 26 0:52 / "
@ -402,13 +402,13 @@ void tst_QStorageInfo::testParseMountInfo_data()
"workdir=/var/lib/docker/overlay2/f3fbad5eedef71145f00729f0826ea8c44defcfec8c92c58aee0aa2c5ea3fa3a/work,"
"index=off,xino=off\n"_ba
<< MountInfo{"/var/lib/docker/overlay2/f3fbad5eedef71145f00729f0826ea8c44defcfec8c92c58aee0aa2c5ea3fa3a/merged",
"overlay", "overlay", "", makedev(0, 52)};
"overlay", "overlay", "", makedev(0, 52), 727};
QTest::newRow("sshfs-src-device-not-start-with-slash")
<< "128 92 0:64 / /mnt-point rw,nosuid,nodev,relatime shared:234 - "
"fuse.sshfs admin@192.168.1.2:/storage/emulated/0 rw,user_id=1000,group_id=1000\n"_ba
<< MountInfo{"/mnt-point", "fuse.sshfs",
"admin@192.168.1.2:/storage/emulated/0", "", makedev(0, 64)};
"admin@192.168.1.2:/storage/emulated/0", "", makedev(0, 64), 128};
}
void tst_QStorageInfo::testParseMountInfo()
@ -424,6 +424,7 @@ void tst_QStorageInfo::testParseMountInfo()
QCOMPARE(a.device, expected.device);
QCOMPARE(a.fsRoot, expected.fsRoot);
QCOMPARE(a.stDev, expected.stDev);
QCOMPARE(a.mntid, expected.mntid);
}
void tst_QStorageInfo::testParseMountInfo_filtered_data()