Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qstorageinfo_linux.cpp
Go to the documentation of this file.
1// Copyright (C) 2021 The Qt Company Ltd.
2// Copyright (C) 2014 Ivan Komissarov <ABBAPOH@gmail.com>
3// Copyright (C) 2016 Intel Corporation.
4// Copyright (C) 2023 Ahmad Samir <a.samirh78@gmail.com>
5// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
6// Qt-Security score:significant reason:default
7
9
10#include <private/qcore_unix_p.h>
11#include <private/qlocale_tools_p.h>
12#include <private/qtools_p.h>
13
14#include <QtCore/qdirlisting.h>
15#include <QtCore/qsystemdetection.h>
16
17#include <q20memory.h>
18
19#include <sys/ioctl.h>
20#include <sys/stat.h>
21#include <sys/statfs.h>
22
23// so we don't have to #include <linux/fs.h>, which is known to cause conflicts
24#ifndef FSLABEL_MAX
25# define FSLABEL_MAX 256
26#endif
27#ifndef FS_IOC_GETFSLABEL
28# define FS_IOC_GETFSLABEL _IOR(0x94, 49, char[FSLABEL_MAX])
29#endif
30
31// or <linux/statfs.h>
32#ifndef ST_RDONLY
33# define ST_RDONLY 0x0001 /* mount read-only */
34#endif
35
36#if defined(Q_OS_ANDROID)
37// statx() is disabled on Android because quite a few systems
38// come with sandboxes that kill applications that make system calls outside a
39// whitelist and several Android vendors can't be bothered to update the list.
40# undef STATX_BASIC_STATS
41#include <private/qjnihelpers_p.h>
42#endif
43
44QT_BEGIN_NAMESPACE
45
46using namespace Qt::StringLiterals;
47
48static const char MountInfoPath[] = "/proc/self/mountinfo";
49
50static std::optional<dev_t> deviceNumber(QByteArrayView devno)
51{
52 // major:minor
53 auto it = devno.cbegin();
54 auto r = qstrntoll(it, devno.size(), 10);
55 if (!r.ok())
56 return std::nullopt;
57 int rdevmajor = int(r.result);
58 it += r.used;
59
60 if (*it != ':')
61 return std::nullopt;
62
63 r = qstrntoll(++it, devno.size() - r.used + 1, 10);
64 if (!r.ok())
65 return std::nullopt;
66
67 return makedev(rdevmajor, r.result);
68}
69
70// Helper function to parse paths that the kernel inserts escape sequences
71// for.
72static QByteArray parseMangledPath(QByteArrayView path)
73{
74 // The kernel escapes with octal the following characters:
75 // space ' ', tab '\t', backslash '\\', and newline '\n'
76 // See:
77 // https://codebrowser.dev/linux/linux/fs/proc_namespace.c.html#show_mountinfo
78 // https://codebrowser.dev/linux/linux/fs/seq_file.c.html#mangle_path
79
80 QByteArray ret(path.size(), '\0');
81 char *dst = ret.data();
82 const char *src = path.data();
83 const char *srcEnd = path.data() + path.size();
84 while (src != srcEnd) {
85 switch (*src) {
86 case ' ': // Shouldn't happen
87 return {};
88
89 case '\\': {
90 // It always uses exactly three octal characters.
91 ++src;
92 char c = (*src++ - '0') << 6;
93 c |= (*src++ - '0') << 3;
94 c |= (*src++ - '0');
95 *dst++ = c;
96 break;
97 }
98
99 default:
100 *dst++ = *src++;
101 break;
102 }
103 }
104 // If "path" contains any of the characters this method is demangling,
105 // "ret" would be oversized with extra '\0' characters at the end.
106 ret.resize(dst - ret.data());
107 return ret;
108}
109
110// Indexes into the "fields" std::array in parseMountInfo()
111static constexpr short MountId = 0;
112// static constexpr short ParentId = 1;
113static constexpr short DevNo = 2;
114static constexpr short FsRoot = 3;
115static constexpr short MountPoint = 4;
116static constexpr short MountOptions = 5;
117// static constexpr short OptionalFields = 6;
118// static constexpr short Separator = 7;
119static constexpr short FsType = 8;
120static constexpr short MountSource = 9;
121static constexpr short SuperOptions = 10;
122static constexpr short FieldCount = 11;
123
124// Splits a line from /proc/self/mountinfo into fields; fields are separated
125// by a single space.
126static void tokenizeLine(std::array<QByteArrayView, FieldCount> &fields, QByteArrayView line)
127{
128 size_t fieldIndex = 0;
129 qsizetype from = 0;
130 const char *begin = line.data();
131 const qsizetype len = line.size();
132 qsizetype spaceIndex = -1;
133 while ((spaceIndex = line.indexOf(' ', from)) != -1 && fieldIndex < FieldCount) {
134 fields[fieldIndex] = QByteArrayView{begin + from, begin + spaceIndex};
135 from = spaceIndex;
136
137 // Skip "OptionalFields" and Separator fields
138 if (fieldIndex == MountOptions) {
139 static constexpr char separatorField[] = " - ";
140 const qsizetype sepIndex = line.indexOf(separatorField, from);
141 if (sepIndex == -1) {
142 qCWarning(lcStorageInfo,
143 "Malformed line (missing '-' separator field) while parsing '%s':\n%s",
144 MountInfoPath, line.constData());
145 fields.fill({});
146 return;
147 }
148
149 from = sepIndex + strlen(separatorField);
150 // Continue parsing at FsType field
151 fieldIndex = FsType;
152 continue;
153 }
154
155 if (from + 1 < len)
156 ++from; // Skip the space at spaceIndex
157
158 ++fieldIndex;
159 }
160
161 // Currently we don't use the last field, so just check the index
162 if (fieldIndex != SuperOptions) {
163 qCInfo(lcStorageInfo,
164 "Expected %d fields while parsing line from '%s', but found %zu instead:\n%.*s",
165 FieldCount, MountInfoPath, fieldIndex, int(line.size()), line.data());
166 fields.fill({});
167 }
168}
169
170std::vector<MountInfo> doParseMountInfo(const QByteArray &mountinfo, FilterMountInfo filter)
171{
172 // https://www.kernel.org/doc/Documentation/filesystems/proc.txt:
173 // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
174 // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
175
176 auto it = mountinfo.cbegin();
177 const auto end = mountinfo.cend();
178 auto nextLine = [&it, &end]() -> QByteArrayView {
179 auto nIt = std::find(it, end, '\n');
180 if (nIt != end) {
181 QByteArrayView ba(it, nIt);
182 it = ++nIt; // Advance
183 return ba;
184 }
185 return {};
186 };
187
188 std::vector<MountInfo> infos;
189 std::array<QByteArrayView, FieldCount> fields;
190 QByteArrayView line;
191
192 auto checkField = [&line](QByteArrayView field) {
193 if (field.isEmpty()) {
194 qDebug("Failed to parse line from %s:\n%.*s", MountInfoPath, int(line.size()),
195 line.data());
196 return false;
197 }
198 return true;
199 };
200
201 // mountinfo has a stable format, no empty lines
202 while (!(line = nextLine()).isEmpty()) {
203 fields.fill({});
204 tokenizeLine(fields, line);
205
206 MountInfo info;
207 if (auto r = qstrntoll(fields[MountId].data(), fields[MountId].size(), 10); r.ok()) {
208 info.mntid = r.result;
209 } else {
210 checkField({});
211 continue;
212 }
213
214 QByteArray mountP = parseMangledPath(fields[MountPoint]);
215 if (!checkField(mountP))
216 continue;
217 info.mountPoint = QFile::decodeName(mountP);
218
219 if (!checkField(fields[FsType]))
220 continue;
221 info.fsType = fields[FsType].toByteArray();
222
223 if (filter == FilterMountInfo::Filtered
224 && !QStorageInfoPrivate::shouldIncludeFs(info.mountPoint, info.fsType))
225 continue;
226
227 std::optional<dev_t> devno = deviceNumber(fields[DevNo]);
228 if (!devno) {
229 checkField({});
230 continue;
231 }
232 info.stDev = *devno;
233
234 QByteArrayView fsRootView = fields[FsRoot];
235 if (!checkField(fsRootView))
236 continue;
237
238 // If the filesystem root is "/" -- it's not a *sub*-volume/bind-mount,
239 // in that case we leave info.fsRoot empty
240 if (fsRootView != "/") {
241 info.fsRoot = parseMangledPath(fsRootView);
242 if (!checkField(info.fsRoot))
243 continue;
244 }
245
246 info.device = parseMangledPath(fields[MountSource]);
247 if (!checkField(info.device))
248 continue;
249
250 infos.push_back(std::move(info));
251 }
252 return infos;
253}
254
255namespace {
256struct AutoFileDescriptor
257{
258 int fd = -1;
259 AutoFileDescriptor(const QString &path, int mode = QT_OPEN_RDONLY)
260 : fd(qt_safe_open(QFile::encodeName(path), mode))
261 {}
262 ~AutoFileDescriptor() { if (fd >= 0) qt_safe_close(fd); }
263 operator int() const noexcept { return fd; }
264};
265}
266
267// udev encodes the labels with ID_LABEL_FS_ENC which is done with
268// blkid_encode_string(). Within this function some 1-byte utf-8
269// characters not considered safe (e.g. '\' or ' ') are encoded as hex
270static QString decodeFsEncString(QString &&str)
271{
272 using namespace QtMiscUtils;
273 qsizetype start = str.indexOf(u'\\');
274 if (start < 0)
275 return std::move(str);
276
277 // decode in-place
278 QString decoded = std::move(str);
279 auto ptr = reinterpret_cast<char16_t *>(decoded.begin());
280 qsizetype in = start;
281 qsizetype out = start;
282 qsizetype size = decoded.size();
283
284 while (in < size) {
285 Q_ASSERT(ptr[in] == u'\\');
286 if (size - in >= 4 && ptr[in + 1] == u'x') { // we need four characters: \xAB
287 int c = fromHex(ptr[in + 2]) << 4;
288 c |= fromHex(ptr[in + 3]);
289 if (Q_UNLIKELY(c < 0))
290 c = QChar::ReplacementCharacter; // bad hex sequence
291 ptr[out++] = c;
292 in += 4;
293 }
294
295 for ( ; in < size; ++in) {
296 char16_t c = ptr[in];
297 if (c == u'\\')
298 break;
299 ptr[out++] = c;
300 }
301 }
302 decoded.resize(out);
303 return decoded;
304}
305
306static inline dev_t deviceIdForPath(const QString &device)
307{
308 QT_STATBUF st;
309 if (QT_STAT(QFile::encodeName(device), &st) < 0)
310 return 0;
311 return st.st_dev;
312}
313
314static inline quint64 mountIdForPath(int fd)
315{
316 if (fd < 0)
317 return 0;
318#if defined(STATX_BASIC_STATS) && defined(STATX_MNT_ID)
319 // STATX_MNT_ID was added in kernel v5.8
320 struct statx st;
321 int r = statx(fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, STATX_MNT_ID, &st);
322 if (r == 0 && (st.stx_mask & STATX_MNT_ID))
323 return st.stx_mnt_id;
324#endif
325 return 0;
326}
327
328static inline quint64 retrieveDeviceId(const QByteArray &device, quint64 deviceId = 0)
329{
330 // major = 0 implies an anonymous block device, so we need to stat() the
331 // actual device to get its dev_t. This is required for btrfs (and possibly
332 // others), which always uses them for all the subvolumes (including the
333 // root):
334 // https://codebrowser.dev/linux/linux/fs/btrfs/disk-io.c.html#btrfs_init_fs_root
335 // https://codebrowser.dev/linux/linux/fs/super.c.html#get_anon_bdev
336 // For everything else, we trust the parameter.
337 if (major(deviceId) != 0)
338 return deviceId;
339
340 // don't even try to stat() a relative path or "/"
341 if (device.size() < 2 || !device.startsWith('/'))
342 return 0;
343
344 QT_STATBUF st;
345 if (QT_STAT(device, &st) < 0)
346 return 0;
347 if (!S_ISBLK(st.st_mode))
348 return 0;
349 return st.st_rdev;
350}
351
353{
354 static const char pathDiskByLabel[] = "/dev/disk/by-label";
355 static constexpr auto LabelFileFilter = QDirListing::IteratorFlag::IncludeHidden;
356 return QDirListing(QLatin1StringView(pathDiskByLabel), LabelFileFilter);
357}
358
359static inline auto retrieveLabels()
360{
361 struct Entry {
362 QString label;
363 quint64 deviceId;
364 };
365 QList<Entry> result;
366
367 for (const auto &dirEntry : devicesByLabel()) {
368 quint64 deviceId = retrieveDeviceId(QFile::encodeName(dirEntry.filePath()));
369 if (!deviceId)
370 continue;
371 result.emplaceBack(Entry{ decodeFsEncString(dirEntry.fileName()), deviceId });
372 }
373 return result;
374}
375
377{
378 // FS_IOC_GETFSLABEL was introduced in v4.18; previously it was btrfs-specific.
379 if (fd < 0)
380 return std::nullopt;
381
382 // Note: it doesn't append the null terminator (despite what the man page
383 // says) and the return code on success (0) does not indicate the length.
384 char label[FSLABEL_MAX] = {};
385 int r = ioctl(fd, FS_IOC_GETFSLABEL, &label);
386 if (r < 0)
387 return std::nullopt;
388 return QString::fromUtf8(label);
389}
390
391static inline QString retrieveLabel(const QStorageInfoPrivate &d, int fd, quint64 deviceId)
392{
393 if (auto label = retrieveLabelViaIoctl(fd))
394 return *label;
395
396 deviceId = retrieveDeviceId(d.device, deviceId);
397 if (!deviceId)
398 return QString();
399
400 for (const auto &dirEntry : devicesByLabel()) {
401 if (retrieveDeviceId(QFile::encodeName(dirEntry.filePath())) == deviceId)
402 return decodeFsEncString(dirEntry.fileName());
403 }
404 return QString();
405}
406
407void QStorageInfoPrivate::retrieveVolumeInfo()
408{
409 struct statfs64 statfs_buf;
410 int result;
411 QT_EINTR_LOOP(result, statfs64(QFile::encodeName(rootPath).constData(), &statfs_buf));
412 valid = ready = (result == 0);
413 if (valid) {
414 bytesTotal = statfs_buf.f_blocks * statfs_buf.f_frsize;
415 bytesFree = statfs_buf.f_bfree * statfs_buf.f_frsize;
416 bytesAvailable = statfs_buf.f_bavail * statfs_buf.f_frsize;
417 blockSize = int(statfs_buf.f_bsize);
418 readOnly = (statfs_buf.f_flags & ST_RDONLY) != 0;
419 }
420}
421
423{
424 QFile file(u"/proc/self/mountinfo"_s);
425 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
426 return {};
427
428 QByteArray mountinfo = file.readAll();
429 file.close();
430
431 return doParseMountInfo(mountinfo, filter);
432}
433
434void QStorageInfoPrivate::doStat()
435{
436#ifdef Q_OS_ANDROID
437 if (QtAndroidPrivate::isUncompressedNativeLibs()) {
438 // We need to pass the actual file path on the file system to statfs64
439 QString possibleApk = QtAndroidPrivate::resolveApkPath(rootPath);
440 if (!possibleApk.isEmpty())
441 rootPath = possibleApk;
442 }
443#endif
444
445 retrieveVolumeInfo();
446 if (!ready)
447 return;
448
449 rootPath = QFileInfo(rootPath).canonicalFilePath();
450 if (rootPath.isEmpty())
451 return;
452
453 std::vector<MountInfo> infos = parseMountInfo();
454 if (infos.empty()) {
455 rootPath = u'/';
456 return;
457 }
458
459 MountInfo *best = nullptr;
460 AutoFileDescriptor fd(rootPath);
461 if (quint64 mntid = mountIdForPath(fd)) {
462 // We have the mount ID for this path, so find the matching line.
463 auto it = std::find_if(infos.begin(), infos.end(),
464 [mntid](const MountInfo &info) { return info.mntid == mntid; });
465 if (it != infos.end())
466 best = q20::to_address(it);
467 } else {
468 // We have failed to get the mount ID for this path, usually because
469 // the path cannot be open()ed by this user (e.g., /root), so we fall
470 // back to a string search.
471 // We iterate over the /proc/self/mountinfo list backwards because then any
472 // matching isParentOf must be the actual mount point because it's the most
473 // recent mount on that path. Linux does allow mounting over non-empty
474 // directories, such as in:
475 // # mount | tail -2
476 // tmpfs on /tmp/foo/bar type tmpfs (rw,relatime,inode64)
477 // tmpfs on /tmp/foo type tmpfs (rw,relatime,inode64)
478 //
479 // We try to match the device ID in case there's a mount --move.
480 // We can't *rely* on it because some filesystems like btrfs will assign
481 // device IDs to subvolumes that aren't listed in /proc/self/mountinfo.
482
483 const QString oldRootPath = std::exchange(rootPath, QString());
484 const dev_t rootPathDevId = deviceIdForPath(oldRootPath);
485 for (auto it = infos.rbegin(); it != infos.rend(); ++it) {
486 if (!isParentOf(it->mountPoint, oldRootPath))
487 continue;
488 if (rootPathDevId == it->stDev) {
489 // device ID matches; this is definitely the best option
490 best = q20::to_address(it);
491 break;
492 }
493 if (!best) {
494 // if we can't find a device ID match, this parent path is probably
495 // the correct one
496 best = q20::to_address(it);
497 }
498 }
499 }
500 if (best) {
501 auto stDev = best->stDev;
502 setFromMountInfo(std::move(*best));
503 name = retrieveLabel(*this, fd, stDev);
504 }
505}
506
507QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
508{
509 std::vector<MountInfo> infos = parseMountInfo(FilterMountInfo::Filtered);
510 if (infos.empty())
511 return QList{root()};
512
513 std::optional<decltype(retrieveLabels())> labelMap;
514 auto labelForDevice = [&labelMap](const QStorageInfoPrivate &d, int fd, quint64 devid) {
515 if (d.fileSystemType == "tmpfs")
516 return QString();
517
518 if (auto label = retrieveLabelViaIoctl(fd))
519 return *label;
520
521 devid = retrieveDeviceId(d.device, devid);
522 if (!devid)
523 return QString();
524
525 if (!labelMap)
526 labelMap = retrieveLabels();
527 for (auto &[deviceLabel, deviceId] : std::as_const(*labelMap)) {
528 if (devid == deviceId)
529 return deviceLabel;
530 }
531 return QString();
532 };
533
534 QList<QStorageInfo> volumes;
535 volumes.reserve(infos.size());
536 for (auto it = infos.begin(); it != infos.end(); ++it) {
537 MountInfo &info = *it;
538 AutoFileDescriptor fd(info.mountPoint);
539
540 // find out if the path as we see it matches this line from mountinfo
541 quint64 mntid = mountIdForPath(fd);
542 if (mntid == 0) {
543 // statx failed, so scan the later lines to see if any is a parent
544 // to this
545 auto isParent = [&info](const MountInfo &maybeParent) {
546 return isParentOf(maybeParent.mountPoint, info.mountPoint);
547 };
548 if (std::find_if(it + 1, infos.end(), isParent) != infos.end())
549 continue;
550 } else if (mntid != info.mntid) {
551 continue;
552 }
553
554 const auto infoStDev = info.stDev;
555 QStorageInfoPrivate d(std::move(info));
556 d.retrieveVolumeInfo();
557 if (d.bytesTotal <= 0 && d.rootPath != u'/')
558 continue;
559 d.name = labelForDevice(d, fd, infoStDev);
560 volumes.emplace_back(QStorageInfo(*new QStorageInfoPrivate(std::move(d))));
561 }
562 return volumes;
563}
564
565QT_END_NAMESPACE
static QString retrieveLabel(const QStorageInfoPrivate &d, int fd, quint64 deviceId)
static constexpr short MountId
static quint64 retrieveDeviceId(const QByteArray &device, quint64 deviceId=0)
static const char MountInfoPath[]
static constexpr short MountPoint
static constexpr short MountSource
static constexpr short FieldCount
static quint64 mountIdForPath(int fd)
static constexpr short MountOptions
std::vector< MountInfo > doParseMountInfo(const QByteArray &mountinfo, FilterMountInfo filter)
static constexpr short FsType
static std::optional< dev_t > deviceNumber(QByteArrayView devno)
static std::optional< QString > retrieveLabelViaIoctl(int fd)
#define FS_IOC_GETFSLABEL
static constexpr short DevNo
#define ST_RDONLY
static QByteArray parseMangledPath(QByteArrayView path)
static constexpr short SuperOptions
static QDirListing devicesByLabel()
static constexpr short FsRoot
static QString decodeFsEncString(QString &&str)
static std::vector< MountInfo > parseMountInfo(FilterMountInfo filter=FilterMountInfo::All)
static auto retrieveLabels()
static void tokenizeLine(std::array< QByteArrayView, FieldCount > &fields, QByteArrayView line)
static dev_t deviceIdForPath(const QString &device)
#define FSLABEL_MAX