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
8
9#include <private/qcore_unix_p.h>
10#include <private/qlocale_tools_p.h>
11#include <private/qtools_p.h>
12
13#include <QtCore/qdirlisting.h>
14#include <QtCore/qsystemdetection.h>
15
16#include <q20memory.h>
17
18#include <sys/ioctl.h>
19#include <sys/stat.h>
20#include <sys/statfs.h>
21
22// so we don't have to #include <linux/fs.h>, which is known to cause conflicts
23#ifndef FSLABEL_MAX
24# define FSLABEL_MAX 256
25#endif
26#ifndef FS_IOC_GETFSLABEL
27# define FS_IOC_GETFSLABEL _IOR(0x94, 49, char[FSLABEL_MAX])
28#endif
29
30// or <linux/statfs.h>
31#ifndef ST_RDONLY
32# define ST_RDONLY 0x0001 /* mount read-only */
33#endif
34
35#if defined(Q_OS_ANDROID)
36// statx() is disabled on Android because quite a few systems
37// come with sandboxes that kill applications that make system calls outside a
38// whitelist and several Android vendors can't be bothered to update the list.
39# undef STATX_BASIC_STATS
40#include <private/qjnihelpers_p.h>
41#endif
42
43QT_BEGIN_NAMESPACE
44
45using namespace Qt::StringLiterals;
46
47static const char MountInfoPath[] = "/proc/self/mountinfo";
48
49static std::optional<dev_t> deviceNumber(QByteArrayView devno)
50{
51 // major:minor
52 auto it = devno.cbegin();
53 auto r = qstrntoll(it, devno.size(), 10);
54 if (!r.ok())
55 return std::nullopt;
56 int rdevmajor = int(r.result);
57 it += r.used;
58
59 if (*it != ':')
60 return std::nullopt;
61
62 r = qstrntoll(++it, devno.size() - r.used + 1, 10);
63 if (!r.ok())
64 return std::nullopt;
65
66 return makedev(rdevmajor, r.result);
67}
68
69// Helper function to parse paths that the kernel inserts escape sequences
70// for.
71static QByteArray parseMangledPath(QByteArrayView path)
72{
73 // The kernel escapes with octal the following characters:
74 // space ' ', tab '\t', backslash '\\', and newline '\n'
75 // See:
76 // https://codebrowser.dev/linux/linux/fs/proc_namespace.c.html#show_mountinfo
77 // https://codebrowser.dev/linux/linux/fs/seq_file.c.html#mangle_path
78
79 QByteArray ret(path.size(), '\0');
80 char *dst = ret.data();
81 const char *src = path.data();
82 const char *srcEnd = path.data() + path.size();
83 while (src != srcEnd) {
84 switch (*src) {
85 case ' ': // Shouldn't happen
86 return {};
87
88 case '\\': {
89 // It always uses exactly three octal characters.
90 ++src;
91 char c = (*src++ - '0') << 6;
92 c |= (*src++ - '0') << 3;
93 c |= (*src++ - '0');
94 *dst++ = c;
95 break;
96 }
97
98 default:
99 *dst++ = *src++;
100 break;
101 }
102 }
103 // If "path" contains any of the characters this method is demangling,
104 // "ret" would be oversized with extra '\0' characters at the end.
105 ret.resize(dst - ret.data());
106 return ret;
107}
108
109// Indexes into the "fields" std::array in parseMountInfo()
110static constexpr short MountId = 0;
111// static constexpr short ParentId = 1;
112static constexpr short DevNo = 2;
113static constexpr short FsRoot = 3;
114static constexpr short MountPoint = 4;
115static constexpr short MountOptions = 5;
116// static constexpr short OptionalFields = 6;
117// static constexpr short Separator = 7;
118static constexpr short FsType = 8;
119static constexpr short MountSource = 9;
120static constexpr short SuperOptions = 10;
121static constexpr short FieldCount = 11;
122
123// Splits a line from /proc/self/mountinfo into fields; fields are separated
124// by a single space.
125static void tokenizeLine(std::array<QByteArrayView, FieldCount> &fields, QByteArrayView line)
126{
127 size_t fieldIndex = 0;
128 qsizetype from = 0;
129 const char *begin = line.data();
130 const qsizetype len = line.size();
131 qsizetype spaceIndex = -1;
132 while ((spaceIndex = line.indexOf(' ', from)) != -1 && fieldIndex < FieldCount) {
133 fields[fieldIndex] = QByteArrayView{begin + from, begin + spaceIndex};
134 from = spaceIndex;
135
136 // Skip "OptionalFields" and Separator fields
137 if (fieldIndex == MountOptions) {
138 static constexpr char separatorField[] = " - ";
139 const qsizetype sepIndex = line.indexOf(separatorField, from);
140 if (sepIndex == -1) {
141 qCWarning(lcStorageInfo,
142 "Malformed line (missing '-' separator field) while parsing '%s':\n%s",
143 MountInfoPath, line.constData());
144 fields.fill({});
145 return;
146 }
147
148 from = sepIndex + strlen(separatorField);
149 // Continue parsing at FsType field
150 fieldIndex = FsType;
151 continue;
152 }
153
154 if (from + 1 < len)
155 ++from; // Skip the space at spaceIndex
156
157 ++fieldIndex;
158 }
159
160 // Currently we don't use the last field, so just check the index
161 if (fieldIndex != SuperOptions) {
162 qCInfo(lcStorageInfo,
163 "Expected %d fields while parsing line from '%s', but found %zu instead:\n%.*s",
164 FieldCount, MountInfoPath, fieldIndex, int(line.size()), line.data());
165 fields.fill({});
166 }
167}
168
169std::vector<MountInfo> doParseMountInfo(const QByteArray &mountinfo, FilterMountInfo filter)
170{
171 // https://www.kernel.org/doc/Documentation/filesystems/proc.txt:
172 // 36 35 98:0 /mnt1 /mnt2 rw,noatime master:1 - ext3 /dev/root rw,errors=continue
173 // (1)(2)(3) (4) (5) (6) (7) (8) (9) (10) (11)
174
175 auto it = mountinfo.cbegin();
176 const auto end = mountinfo.cend();
177 auto nextLine = [&it, &end]() -> QByteArrayView {
178 auto nIt = std::find(it, end, '\n');
179 if (nIt != end) {
180 QByteArrayView ba(it, nIt);
181 it = ++nIt; // Advance
182 return ba;
183 }
184 return {};
185 };
186
187 std::vector<MountInfo> infos;
188 std::array<QByteArrayView, FieldCount> fields;
189 QByteArrayView line;
190
191 auto checkField = [&line](QByteArrayView field) {
192 if (field.isEmpty()) {
193 qDebug("Failed to parse line from %s:\n%.*s", MountInfoPath, int(line.size()),
194 line.data());
195 return false;
196 }
197 return true;
198 };
199
200 // mountinfo has a stable format, no empty lines
201 while (!(line = nextLine()).isEmpty()) {
202 fields.fill({});
203 tokenizeLine(fields, line);
204
205 MountInfo info;
206 if (auto r = qstrntoll(fields[MountId].data(), fields[MountId].size(), 10); r.ok()) {
207 info.mntid = r.result;
208 } else {
209 checkField({});
210 continue;
211 }
212
213 QByteArray mountP = parseMangledPath(fields[MountPoint]);
214 if (!checkField(mountP))
215 continue;
216 info.mountPoint = QFile::decodeName(mountP);
217
218 if (!checkField(fields[FsType]))
219 continue;
220 info.fsType = fields[FsType].toByteArray();
221
222 if (filter == FilterMountInfo::Filtered
223 && !QStorageInfoPrivate::shouldIncludeFs(info.mountPoint, info.fsType))
224 continue;
225
226 std::optional<dev_t> devno = deviceNumber(fields[DevNo]);
227 if (!devno) {
228 checkField({});
229 continue;
230 }
231 info.stDev = *devno;
232
233 QByteArrayView fsRootView = fields[FsRoot];
234 if (!checkField(fsRootView))
235 continue;
236
237 // If the filesystem root is "/" -- it's not a *sub*-volume/bind-mount,
238 // in that case we leave info.fsRoot empty
239 if (fsRootView != "/") {
240 info.fsRoot = parseMangledPath(fsRootView);
241 if (!checkField(info.fsRoot))
242 continue;
243 }
244
245 info.device = parseMangledPath(fields[MountSource]);
246 if (!checkField(info.device))
247 continue;
248
249 infos.push_back(std::move(info));
250 }
251 return infos;
252}
253
254namespace {
255struct AutoFileDescriptor
256{
257 int fd = -1;
258 AutoFileDescriptor(const QString &path, int mode = QT_OPEN_RDONLY)
259 : fd(qt_safe_open(QFile::encodeName(path), mode))
260 {}
261 ~AutoFileDescriptor() { if (fd >= 0) qt_safe_close(fd); }
262 operator int() const noexcept { return fd; }
263};
264}
265
266// udev encodes the labels with ID_LABEL_FS_ENC which is done with
267// blkid_encode_string(). Within this function some 1-byte utf-8
268// characters not considered safe (e.g. '\' or ' ') are encoded as hex
269static QString decodeFsEncString(QString &&str)
270{
271 using namespace QtMiscUtils;
272 qsizetype start = str.indexOf(u'\\');
273 if (start < 0)
274 return std::move(str);
275
276 // decode in-place
277 QString decoded = std::move(str);
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 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