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 decoded)
271{
272 using namespace QtMiscUtils;
273 const qsizetype start = decoded.indexOf(u'\\');
274 if (start < 0)
275 return decoded;
276
277 // decode in-place
278 auto ptr = reinterpret_cast<char16_t *>(decoded.begin());
279 qsizetype in = start;
280 qsizetype out = start;
281 qsizetype size = decoded.size();
282
283 while (in < size) {
284 Q_ASSERT(ptr[in] == u'\\');
285 if (size - in >= 4 && ptr[in + 1] == u'x') { // we need four characters: \xAB
286 int c = fromHex(ptr[in + 2]) << 4;
287 c |= fromHex(ptr[in + 3]);
288 if (Q_UNLIKELY(c < 0))
289 c = QChar::ReplacementCharacter; // bad hex sequence
290 ptr[out++] = c;
291 in += 4;
292 }
293
294 for ( ; in < size; ++in) {
295 char16_t c = ptr[in];
296 if (c == u'\\')
297 break;
298 ptr[out++] = c;
299 }
300 }
301 decoded.resize(out);
302 return decoded;
303}
304
305static inline dev_t deviceIdForPath(const QString &device)
306{
307 QT_STATBUF st;
308 if (QT_STAT(QFile::encodeName(device), &st) < 0)
309 return 0;
310 return st.st_dev;
311}
312
313static inline quint64 mountIdForPath(int fd)
314{
315 if (fd < 0)
316 return 0;
317#if defined(STATX_BASIC_STATS) && defined(STATX_MNT_ID)
318 // STATX_MNT_ID was added in kernel v5.8
319 struct statx st;
320 int r = statx(fd, "", AT_EMPTY_PATH | AT_NO_AUTOMOUNT, STATX_MNT_ID, &st);
321 if (r == 0 && (st.stx_mask & STATX_MNT_ID))
322 return st.stx_mnt_id;
323#endif
324 return 0;
325}
326
327static inline quint64 retrieveDeviceId(const QByteArray &device, quint64 deviceId = 0)
328{
329 // major = 0 implies an anonymous block device, so we need to stat() the
330 // actual device to get its dev_t. This is required for btrfs (and possibly
331 // others), which always uses them for all the subvolumes (including the
332 // root):
333 // https://codebrowser.dev/linux/linux/fs/btrfs/disk-io.c.html#btrfs_init_fs_root
334 // https://codebrowser.dev/linux/linux/fs/super.c.html#get_anon_bdev
335 // For everything else, we trust the parameter.
336 if (major(deviceId) != 0)
337 return deviceId;
338
339 // don't even try to stat() a relative path or "/"
340 if (device.size() < 2 || !device.startsWith('/'))
341 return 0;
342
343 QT_STATBUF st;
344 if (QT_STAT(device, &st) < 0)
345 return 0;
346 if (!S_ISBLK(st.st_mode))
347 return 0;
348 return st.st_rdev;
349}
350
352{
353 static const char pathDiskByLabel[] = "/dev/disk/by-label";
354 static constexpr auto LabelFileFilter = QDirListing::IteratorFlag::IncludeHidden;
355 return QDirListing(QLatin1StringView(pathDiskByLabel), LabelFileFilter);
356}
357
358static inline auto retrieveLabels()
359{
360 struct Entry {
361 QString label;
362 quint64 deviceId;
363 };
364 QList<Entry> result;
365
366 for (const auto &dirEntry : devicesByLabel()) {
367 quint64 deviceId = retrieveDeviceId(QFile::encodeName(dirEntry.filePath()));
368 if (!deviceId)
369 continue;
370 result.emplaceBack(Entry{ decodeFsEncString(dirEntry.fileName()), deviceId });
371 }
372 return result;
373}
374
376{
377 // FS_IOC_GETFSLABEL was introduced in v4.18; previously it was btrfs-specific.
378 if (fd < 0)
379 return std::nullopt;
380
381 // Note: it doesn't append the null terminator (despite what the man page
382 // says) and the return code on success (0) does not indicate the length.
383 char label[FSLABEL_MAX] = {};
384 int r = ioctl(fd, FS_IOC_GETFSLABEL, &label);
385 if (r < 0)
386 return std::nullopt;
387 return QString::fromUtf8(label);
388}
389
390static inline QString retrieveLabel(const QStorageInfoPrivate &d, int fd, quint64 deviceId)
391{
392 if (auto label = retrieveLabelViaIoctl(fd))
393 return *label;
394
395 deviceId = retrieveDeviceId(d.device, deviceId);
396 if (!deviceId)
397 return QString();
398
399 for (const auto &dirEntry : devicesByLabel()) {
400 if (retrieveDeviceId(QFile::encodeName(dirEntry.filePath())) == deviceId)
401 return decodeFsEncString(dirEntry.fileName());
402 }
403 return QString();
404}
405
406void QStorageInfoPrivate::retrieveVolumeInfo()
407{
408 struct statfs64 statfs_buf;
409 int result;
410 QT_EINTR_LOOP(result, statfs64(QFile::encodeName(rootPath).constData(), &statfs_buf));
411 valid = ready = (result == 0);
412 if (valid) {
413 bytesTotal = statfs_buf.f_blocks * statfs_buf.f_frsize;
414 bytesFree = statfs_buf.f_bfree * statfs_buf.f_frsize;
415 bytesAvailable = statfs_buf.f_bavail * statfs_buf.f_frsize;
416 blockSize = int(statfs_buf.f_bsize);
417 readOnly = (statfs_buf.f_flags & ST_RDONLY) != 0;
418 }
419}
420
422{
423 QFile file(u"/proc/self/mountinfo"_s);
424 if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
425 return {};
426
427 QByteArray mountinfo = file.readAll();
428 file.close();
429
430 return doParseMountInfo(mountinfo, filter);
431}
432
433void QStorageInfoPrivate::doStat()
434{
435#ifdef Q_OS_ANDROID
436 if (QtAndroidPrivate::isUncompressedNativeLibs()) {
437 // We need to pass the actual file path on the file system to statfs64
438 QString possibleApk = QtAndroidPrivate::resolveApkPath(rootPath);
439 if (!possibleApk.isEmpty())
440 rootPath = possibleApk;
441 }
442#endif
443
444 retrieveVolumeInfo();
445 if (!ready)
446 return;
447
448 rootPath = QFileInfo(rootPath).canonicalFilePath();
449 if (rootPath.isEmpty())
450 return;
451
452 std::vector<MountInfo> infos = parseMountInfo();
453 if (infos.empty()) {
454 rootPath = u'/';
455 return;
456 }
457
458 MountInfo *best = nullptr;
459 AutoFileDescriptor fd(rootPath);
460 if (quint64 mntid = mountIdForPath(fd)) {
461 // We have the mount ID for this path, so find the matching line.
462 auto it = std::find_if(infos.begin(), infos.end(),
463 [mntid](const MountInfo &info) { return info.mntid == mntid; });
464 if (it != infos.end())
465 best = q20::to_address(it);
466 } else {
467 // We have failed to get the mount ID for this path, usually because
468 // the path cannot be open()ed by this user (e.g., /root), so we fall
469 // back to a string search.
470 // We iterate over the /proc/self/mountinfo list backwards because then any
471 // matching isParentOf must be the actual mount point because it's the most
472 // recent mount on that path. Linux does allow mounting over non-empty
473 // directories, such as in:
474 // # mount | tail -2
475 // tmpfs on /tmp/foo/bar type tmpfs (rw,relatime,inode64)
476 // tmpfs on /tmp/foo type tmpfs (rw,relatime,inode64)
477 //
478 // We try to match the device ID in case there's a mount --move.
479 // We can't *rely* on it because some filesystems like btrfs will assign
480 // device IDs to subvolumes that aren't listed in /proc/self/mountinfo.
481
482 const QString oldRootPath = std::exchange(rootPath, QString());
483 const dev_t rootPathDevId = deviceIdForPath(oldRootPath);
484 for (auto it = infos.rbegin(); it != infos.rend(); ++it) {
485 if (!isParentOf(it->mountPoint, oldRootPath))
486 continue;
487 if (rootPathDevId == it->stDev) {
488 // device ID matches; this is definitely the best option
489 best = q20::to_address(it);
490 break;
491 }
492 if (!best) {
493 // if we can't find a device ID match, this parent path is probably
494 // the correct one
495 best = q20::to_address(it);
496 }
497 }
498 }
499 if (best) {
500 auto stDev = best->stDev;
501 setFromMountInfo(std::move(*best));
502 name = retrieveLabel(*this, fd, stDev);
503 }
504}
505
506QList<QStorageInfo> QStorageInfoPrivate::mountedVolumes()
507{
508 std::vector<MountInfo> infos = parseMountInfo(FilterMountInfo::Filtered);
509 if (infos.empty())
510 return QList{root()};
511
512 std::optional<decltype(retrieveLabels())> labelMap;
513 auto labelForDevice = [&labelMap](const QStorageInfoPrivate &d, int fd, quint64 devid) {
514 if (d.fileSystemType == "tmpfs")
515 return QString();
516
517 if (auto label = retrieveLabelViaIoctl(fd))
518 return *label;
519
520 devid = retrieveDeviceId(d.device, devid);
521 if (!devid)
522 return QString();
523
524 if (!labelMap)
525 labelMap = retrieveLabels();
526 for (auto &[deviceLabel, deviceId] : std::as_const(*labelMap)) {
527 if (devid == deviceId)
528 return deviceLabel;
529 }
530 return QString();
531 };
532
533 QList<QStorageInfo> volumes;
534 volumes.reserve(infos.size());
535 for (auto it = infos.begin(); it != infos.end(); ++it) {
536 MountInfo &info = *it;
537 AutoFileDescriptor fd(info.mountPoint);
538
539 // find out if the path as we see it matches this line from mountinfo
540 quint64 mntid = mountIdForPath(fd);
541 if (mntid == 0) {
542 // statx failed, so scan the later lines to see if any is a parent
543 // to this
544 auto isParent = [&info](const MountInfo &maybeParent) {
545 return isParentOf(maybeParent.mountPoint, info.mountPoint);
546 };
547 if (std::find_if(it + 1, infos.end(), isParent) != infos.end())
548 continue;
549 } else if (mntid != info.mntid) {
550 continue;
551 }
552
553 const auto infoStDev = info.stDev;
554 QStorageInfoPrivate d(std::move(info));
555 d.retrieveVolumeInfo();
556 if (d.bytesTotal <= 0 && d.rootPath != u'/')
557 continue;
558 d.name = labelForDevice(d, fd, infoStDev);
559 volumes.emplace_back(QStorageInfo(*new QStorageInfoPrivate(std::move(d))));
560 }
561 return volumes;
562}
563
564QT_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 std::vector< MountInfo > parseMountInfo(FilterMountInfo filter=FilterMountInfo::All)
static auto retrieveLabels()
static QString decodeFsEncString(QString decoded)
static void tokenizeLine(std::array< QByteArrayView, FieldCount > &fields, QByteArrayView line)
static dev_t deviceIdForPath(const QString &device)
#define FSLABEL_MAX