diff --git a/src/corelib/io/qstorageinfo_linux.cpp b/src/corelib/io/qstorageinfo_linux.cpp index a08e53daa16..62efac667fd 100644 --- a/src/corelib/io/qstorageinfo_linux.cpp +++ b/src/corelib/io/qstorageinfo_linux.cpp @@ -13,6 +13,7 @@ #include #include +#include #include // so we don't have to #include , 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 @@ -212,33 +246,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) { @@ -279,12 +325,21 @@ QList 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)); diff --git a/src/corelib/io/qstorageinfo_linux_p.h b/src/corelib/io/qstorageinfo_linux_p.h index 6f5e107ec6c..ae6c1a38592 100644 --- a/src/corelib/io/qstorageinfo_linux_p.h +++ b/src/corelib/io/qstorageinfo_linux_p.h @@ -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; diff --git a/src/corelib/io/qstorageinfo_p.h b/src/corelib/io/qstorageinfo_p.h index 3af4b81ca4b..3b049cdfd47 100644 --- a/src/corelib/io/qstorageinfo_p.h +++ b/src/corelib/io/qstorageinfo_p.h @@ -69,6 +69,7 @@ public: QByteArray device; QByteArray fsRoot; dev_t stDev = 0; + quint64 mntid = 0; }; void setFromMountInfo(MountInfo &&info) diff --git a/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp b/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp index 49dbadd89ab..fd8b19f6206 100644 --- a/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp +++ b/tests/auto/corelib/io/qstorageinfo/tst_qstorageinfo.cpp @@ -312,64 +312,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 " @@ -378,7 +378,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 / " @@ -395,13 +395,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() @@ -417,6 +417,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()