QStorageInfo/Linux: fix setPath() for paths mounted over

Linux allows admins to mount new filesystems over non-empty paths
(though I managed to do so on FreeBSD and macOS too), so we must find
the most recent mount that applies to this path instead of the longest
matching mountpoint. We do that by scanning the /proc/self/mountinfo
list backwards and thus any matching isParentOf() must be the correct
one.

 # mkdir -p /tmp/foo/bar
 # mount -t tmpfs tmpfs /tmp/foo/bar
 # mount -t tmpfs -o size=1M tmpfs /tmp/foo
 $ ./tests/manual/qstorageinfo/qstorageinfo /tmp/foo/bar
 Filesystem (Type)                  Size  Available BSize  Label                Mounted on
 tmpfs                     RW       1024       1024  4096                       /tmp/foo

But we must guard against an earlier mount still being (somehow)
accessible. We've seen this in the CI, where /run is earlier than / but
still somehow accessible -- I guess this is one or a pair of mount
--move.

An additional benefit is that don't even attempt to compare to the
virtual filesystems mounted by the system early after boot, if what
we're looking for isn't the root.

See next commit for a fix for QStorageInfo::mountedVolumes().

Change-Id: I79e700614d034281bf55fffd178f8befc5e80edb
Reviewed-by: Ahmad Samir <a.samirh78@gmail.com>
This commit is contained in:
Thiago Macieira 2023-10-19 08:30:46 -07:00
parent 9df2b7ffa4
commit 1cd6c6c69e

View File

@ -59,6 +59,14 @@ static QString decodeFsEncString(const QString &str)
return decoded;
}
static inline dev_t deviceIdForPath(const QString &device)
{
QT_STATBUF st;
if (QT_STAT(QFile::encodeName(device), &st) < 0)
return 0;
return st.st_dev;
}
static inline QString retrieveLabel(const QByteArray &device)
{
static const char pathDiskByLabel[] = "/dev/disk/by-label";
@ -136,24 +144,25 @@ void QStorageInfoPrivate::initRootPath()
return;
}
qsizetype maxLength = 0;
const QString oldRootPath = rootPath;
rootPath.clear();
MountInfo *bestInfo = nullptr;
for (MountInfo &info : infos) {
// we try to find most suitable entry
qsizetype mpSize = info.mountPoint.size();
if (maxLength < mpSize && isParentOf(info.mountPoint, oldRootPath)) {
bestInfo = &info;
maxLength = mpSize;
}
}
if (bestInfo) {
rootPath = std::move(bestInfo->mountPoint);
device = std::move(bestInfo->device);
fileSystemType = std::move(bestInfo->fsType);
subvolume = std::move(bestInfo->fsRoot);
// 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)
// But just in case there's a mount --move, we ensure the device ID does
// match.
const QString oldRootPath = std::exchange(rootPath, QString());
const dev_t rootPathDevId = deviceIdForPath(oldRootPath);
for (auto it = infos.rbegin(); it != infos.rend(); ++it) {
if (rootPathDevId != it->stDev || !isParentOf(it->mountPoint, oldRootPath))
continue;
rootPath = std::move(it->mountPoint);
device = std::move(it->device);
fileSystemType = std::move(it->fsType);
subvolume = std::move(it->fsRoot);
return;
}
}