Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// Copyright (C) 2014 Ivan Komissarov <ABBAPOH@gmail.com>
// Copyright (C) 2016 Intel Corporation.
// Copyright (C) 2023 Ahmad Samir <a.samirh78@gmail.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qstorageinfo_linux_p.h"
#include <private/qcore_unix_p.h>
#include <private/qlocale_tools_p.h>
#include <private/qtools_p.h>
#include <QtCore/qdirlisting.h>
#include <QtCore/qsystemdetection.h>
#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
#ifndef FSLABEL_MAX
# define FSLABEL_MAX 256
#endif
#ifndef FS_IOC_GETFSLABEL
# define FS_IOC_GETFSLABEL _IOR(0x94, 49, char[FSLABEL_MAX])
#endif
// or <linux/statfs.h>
#ifndef ST_RDONLY
# 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
#include <private/qjnihelpers_p.h>
#endif
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
static const char MountInfoPath[] = "/proc/self/mountinfo";
static std::optional<dev_t> deviceNumber(QByteArrayView devno)
{
// major:minor
auto it = devno.cbegin();
auto r = qstrntoll(it, devno.size(), 10);
if (!r.ok())
return std::nullopt;
int rdevmajor = int(r.result);
it += r.used;
if (*it != ':')
return std::nullopt;
r = qstrntoll(++it, devno.size() - r.used + 1, 10);
if (!r.ok())
return std::nullopt;
return makedev(rdevmajor, r.result);
}
// Helper function to parse paths that the kernel inserts escape sequences
// for.
static QByteArray parseMangledPath(QByteArrayView path)
{
// The kernel escapes with octal the following characters:
// space ' ', tab '\t', backslash '\\', and newline '\n'
// See:
QByteArray ret(path.size(), '\0');
char *dst = ret.data();
const char *src = path.data();
const char *srcEnd = path.data() + path.size();
while (src != srcEnd) {
switch (*src) {
case ' ': // Shouldn't happen
return {};
case '\\': {
// It always uses exactly three octal characters.
++src;
char c = (*src++ - '0') << 6;
c |= (*src++ - '0') << 3;
c |= (*src++ - '0');
*dst++ = c;
break;
}
default:
*dst++ = *src++;
break;
}
}
// If "path" contains any of the characters this method is demangling,
// "ret" would be oversized with extra '\0' characters at the end.
ret.resize(dst - ret.data());
return ret;
}
// Indexes into the "fields" std::array in parseMountInfo()
static constexpr short MountId = 0;
// static constexpr short ParentId = 1;
static constexpr short DevNo = 2;
static constexpr short FsRoot = 3;
static constexpr short MountPoint = 4;
static constexpr short MountOptions = 5;
// static constexpr short OptionalFields = 6;
// static constexpr short Separator = 7;
static constexpr short FsType = 8;
static constexpr short MountSource = 9;
static constexpr short SuperOptions = 10;
static constexpr short FieldCount = 11;
// Splits a line from /proc/self/mountinfo into fields; fields are separated
// by a single space.
static void tokenizeLine(std::array<QByteArrayView, FieldCount> &fields, QByteArrayView line)
{
size_t fieldIndex = 0;
qsizetype from = 0;
const char *begin = line.data();
const qsizetype len = line.size();
qsizetype spaceIndex = -1;
while ((spaceIndex = line.indexOf(' ', from)) != -1 && fieldIndex < FieldCount) {
fields[fieldIndex] = QByteArrayView{begin + from, begin + spaceIndex};
from = spaceIndex;
// Skip "OptionalFields" and Separator fields
if (fieldIndex == MountOptions) {
static constexpr char separatorField[] = " - ";
const qsizetype sepIndex = line.indexOf(separatorField, from);
if (sepIndex == -1) {
qCWarning(lcStorageInfo,
"Malformed line (missing '-' separator field) while parsing '%s':\n%s",
MountInfoPath, line.constData());
fields.fill({});
return;
}
from = sepIndex + strlen(separatorField);
// Continue parsing at FsType field
fieldIndex = FsType;
continue;
}
if (from + 1 < len)
++from; // Skip the space at spaceIndex
++fieldIndex;
}
// Currently we don't use the last field, so just check the index
if (fieldIndex != SuperOptions) {
qCInfo(lcStorageInfo,
"Expected %d fields while parsing line from '%s', but found %zu instead:\n%.*s",
FieldCount, MountInfoPath, fieldIndex, int(line.size()), line.data());
fields.fill({});
}
}
std::vector<MountInfo> doParseMountInfo(const QByteArray &mountinfo, FilterMountInfo filter)
{
// 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
// (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
auto it = mountinfo.cbegin();
const auto end = mountinfo.cend();
auto nextLine = [&it, &end]() -> QByteArrayView {
auto nIt = std::find(it, end, '\n');
if (nIt != end) {
QByteArrayView ba(it, nIt);
it = ++nIt; // Advance
return ba;
}
return {};
};
std::vector<MountInfo> infos;
std::array<QByteArrayView, FieldCount> fields;
QByteArrayView line;
auto checkField = [&line](QByteArrayView field) {
if (field.isEmpty()) {
qDebug("Failed to parse line from %s:\n%.*s", MountInfoPath, int(line.size()),
line.data());
return false;
}
return true;
};
// mountinfo has a stable format, no empty lines
while (!(line = nextLine()).isEmpty()) {
fields.fill({});
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;
info.mountPoint = QFile::decodeName(mountP);
if (!checkField(fields[FsType]))
continue;
info.fsType = fields[FsType].toByteArray();
if (filter == FilterMountInfo::Filtered
&& !QStorageInfoPrivate::shouldIncludeFs(info.mountPoint, info.fsType))
continue;
std::optional<dev_t> devno = deviceNumber(fields[DevNo]);
if (!devno) {
checkField({});
continue;
}
info.stDev = *devno;
QByteArrayView fsRootView = fields[FsRoot];
if (!checkField(fsRootView))
continue;
// If the filesystem root is "/" -- it's not a *sub*-volume/bind-mount,
// in that case we leave info.fsRoot empty
if (fsRootView != "/") {
info.fsRoot = parseMangledPath(fsRootView);
if (!checkField(info.fsRoot))
continue;
}
info.device = parseMangledPath(fields[MountSource]);
if (!checkField(info.device))
continue;
infos.push_back(std::move(info));
}
return infos;
}
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
static QString decodeFsEncString(QString decoded)
{
using namespace QtMiscUtils;
const qsizetype start = decoded.indexOf(u'\\');
if (start < 0)
return decoded;
// decode in-place
auto ptr = reinterpret_cast<char16_t *>(decoded.begin());
qsizetype in = start;
qsizetype out = start;
qsizetype size = decoded.size();
while (in < size) {
Q_ASSERT(ptr[in] == u'\\');
if (size - in >= 4 && ptr[in + 1] == u'x') { // we need four characters: \xAB
int c = fromHex(ptr[in + 2]) << 4;
c |= fromHex(ptr[in + 3]);
if (Q_UNLIKELY(c < 0))
c = QChar::ReplacementCharacter; // bad hex sequence
ptr[out++] = c;
in += 4;
}
for ( ; in < size; ++in) {
char16_t c = ptr[in];
if (c == u'\\')
break;
ptr[out++] = c;
}
}
decoded.resize(out);
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 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
// actual device to get its dev_t. This is required for btrfs (and possibly
// others), which always uses them for all the subvolumes (including the
// root):
// For everything else, we trust the parameter.
if (major(deviceId) != 0)
return deviceId;
// don't even try to stat() a relative path or "/"
if (device.size() < 2 || !device.startsWith('/'))
return 0;
QT_STATBUF st;
if (QT_STAT(device, &st) < 0)
return 0;
if (!S_ISBLK(st.st_mode))
return 0;
return st.st_rdev;
}
static QDirListing devicesByLabel()
{
static const char pathDiskByLabel[] = "/dev/disk/by-label";
static constexpr auto LabelFileFilter = QDirListing::IteratorFlag::IncludeHidden;
return QDirListing(QLatin1StringView(pathDiskByLabel), LabelFileFilter);
}
static inline auto retrieveLabels()
{
struct Entry {
QString label;
quint64 deviceId;
};
QList<Entry> result;
for (const auto &dirEntry : devicesByLabel()) {
quint64 deviceId = retrieveDeviceId(QFile::encodeName(dirEntry.filePath()));
if (!deviceId)
continue;
result.emplaceBack(Entry{ decodeFsEncString(dirEntry.fileName()), deviceId });
}
return result;
}
static std::optional<QString> retrieveLabelViaIoctl(int fd)
{
// FS_IOC_GETFSLABEL was introduced in v4.18; previously it was btrfs-specific.
if (fd < 0)
return std::nullopt;
// Note: it doesn't append the null terminator (despite what the man page
// says) and the return code on success (0) does not indicate the length.
char label[FSLABEL_MAX] = {};
int r = ioctl(fd, FS_IOC_GETFSLABEL, &label);
if (r < 0)
return std::nullopt;
return QString::fromUtf8(label);
}
static inline QString retrieveLabel(const QStorageInfoPrivate &d, int fd, quint64 deviceId)
{
if (auto label = retrieveLabelViaIoctl(fd))
return *label;
deviceId = retrieveDeviceId(d.device, deviceId);
if (!deviceId)
return QString();
for (const auto &dirEntry : devicesByLabel()) {
if (retrieveDeviceId(QFile::encodeName(dirEntry.filePath())) == deviceId)
return decodeFsEncString(dirEntry.fileName());
}
return QString();
}
void QStorageInfoPrivate::retrieveVolumeInfo()
{
struct statfs64 statfs_buf;
int result;
QT_EINTR_LOOP(result, statfs64(QFile::encodeName(rootPath).constData(), &statfs_buf));
valid = ready = (result == 0);
if (valid) {
bytesTotal = statfs_buf.f_blocks * statfs_buf.f_frsize;
bytesFree = statfs_buf.f_bfree * statfs_buf.f_frsize;
bytesAvailable = statfs_buf.f_bavail * statfs_buf.f_frsize;
blockSize = int(statfs_buf.f_bsize);
readOnly = (statfs_buf.f_flags & ST_RDONLY) != 0;
}
}
static std::vector<MountInfo> parseMountInfo(FilterMountInfo filter = FilterMountInfo::All)
{
QFile file(u"/proc/self/mountinfo"_s);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
QByteArray mountinfo = file.readAll();
file.close();
return doParseMountInfo(mountinfo, filter);
}
void QStorageInfoPrivate::doStat()
{
#ifdef Q_OS_ANDROID
if (QtAndroidPrivate::isUncompressedNativeLibs()) {
// We need to pass the actual file path on the file system to statfs64
QString possibleApk = QtAndroidPrivate::resolveApkPath(rootPath);
if (!possibleApk.isEmpty())
rootPath = possibleApk;
}
#endif
retrieveVolumeInfo();
if (!ready)
return;
QFileInfo fileInfo(rootPath);
if (QString canonicalPath = fileInfo.canonicalFilePath(); !canonicalPath.isEmpty()) {
rootPath = std::move(canonicalPath);
} else {
// The volume is ready but canonization process failed (for example, a
// mount point we're allowed to statfs() but not open(), as can happen
// under SELinux on Android). The absolute path will have to do.
rootPath = fileInfo.absoluteFilePath();
}
std::vector<MountInfo> infos = parseMountInfo();
if (infos.empty()) {
rootPath = u'/';
return;
}
MountInfo *best = nullptr;
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) {
auto stDev = best->stDev;
setFromMountInfo(std::move(*best));
name = retrieveLabel(*this, fd, stDev);
}
}
QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
{
std::vector<MountInfo> infos = parseMountInfo(FilterMountInfo::Filtered);
if (infos.empty())
return QList{root()};
std::optional<decltype(retrieveLabels())> labelMap;
auto labelForDevice = [&labelMap](const QStorageInfoPrivate &d, int fd, quint64 devid) {
if (d.fileSystemType == "tmpfs")
return QString();
if (auto label = retrieveLabelViaIoctl(fd))
return *label;
devid = retrieveDeviceId(d.device, devid);
if (!devid)
return QString();
if (!labelMap)
labelMap = retrieveLabels();
for (auto &[deviceLabel, deviceId] : std::as_const(*labelMap)) {
if (devid == deviceId)
return deviceLabel;
}
return QString();
};
QList<QStorageInfo> volumes;
volumes.reserve(infos.size());
for (auto it = infos.begin(); it != infos.end(); ++it) {
MountInfo &info = *it;
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));
d.retrieveVolumeInfo();
if (d.bytesTotal <= 0 && d.rootPath != u'/')
continue;
d.name = labelForDevice(d, fd, infoStDev);
volumes.emplace_back(QStorageInfo(*new QStorageInfoPrivate(std::move(d))));
}
return volumes;
}
QT_END_NAMESPACE